├── .editorconfig ├── .github ├── pull_request_template.md └── workflows │ ├── destroy-deployment.yml │ ├── packaging.yml │ ├── pull-translation.yml │ ├── push-translation.yml │ ├── push_rockspec.yml │ ├── reusable-test.yml │ ├── stale.yml │ ├── test.yml │ └── upload-translations.yml ├── .gitignore ├── .gitmodules ├── .luacheckrc ├── .luacov ├── CHANGELOG.md ├── CMakeLists.txt ├── LICENSE ├── Makefile ├── README.md ├── doc ├── README.md ├── cleanup.py ├── conf.py ├── crowdin.yaml ├── locale │ └── ru │ │ └── LC_MESSAGES │ │ ├── api_reference.po │ │ ├── getting_started.po │ │ ├── index.po │ │ ├── metrics_reference.po │ │ └── plugins.po ├── monitoring │ ├── api_reference.rst │ ├── getting_started.rst │ ├── getting_started_cartridge.rst │ ├── images │ │ ├── histogram-buckets.png │ │ ├── histogram.png │ │ ├── role-config.png │ │ ├── role-enable.png │ │ └── summary-buckets.png │ ├── index.rst │ ├── install.rst │ ├── metrics_reference.rst │ └── plugins.rst └── requirements.txt ├── example ├── HTTP │ └── latency_v1.lua ├── default_metrics.lua ├── graphite_export.lua ├── latency_observer.lua └── prometheus_export.lua ├── metrics-scm-1.rockspec ├── metrics ├── api.lua ├── cartridge │ ├── failover.lua │ └── issues.lua ├── cfg.lua ├── collectors │ ├── counter.lua │ ├── gauge.lua │ ├── histogram.lua │ ├── shared.lua │ └── summary.lua ├── const.lua ├── http_middleware.lua ├── init.lua ├── plugins │ ├── graphite.lua │ ├── json.lua │ └── prometheus.lua ├── psutils │ ├── cpu.lua │ └── psutils_linux.lua ├── quantile.lua ├── registry.lua ├── stash.lua ├── tarantool.lua ├── tarantool │ ├── clock.lua │ ├── config.lua │ ├── cpu.lua │ ├── event_loop.lua │ ├── fibers.lua │ ├── info.lua │ ├── luajit.lua │ ├── memory.lua │ ├── memtx.lua │ ├── network.lua │ ├── operations.lua │ ├── replicas.lua │ ├── runtime.lua │ ├── slab.lua │ ├── spaces.lua │ ├── system.lua │ └── vinyl.lua ├── utils.lua └── version.lua ├── rpm ├── prebuild.sh └── tarantool-metrics.spec ├── test ├── cfg_test.lua ├── collectors │ ├── counter_test.lua │ ├── gauge_test.lua │ ├── histogram_test.lua │ ├── latency_observer_test.lua │ ├── shared_test.lua │ └── summary_test.lua ├── enable_default_metrics_test.lua ├── entrypoint │ └── srv_basic.lua ├── helper.lua ├── http_middleware_test.lua ├── integration │ ├── cartridge_metrics_test.lua │ ├── highload_test.lua │ └── hotreload_test.lua ├── metrics_test.lua ├── plugins │ ├── graphite_test.lua │ ├── json_test.lua │ └── prometheus_test.lua ├── promtool.lua ├── psutils_linux_test.lua ├── psutils_linux_test_payload │ ├── init.lua │ ├── proc_self_task │ │ ├── 1 │ │ │ └── stat │ │ ├── 12 │ │ │ └── stat │ │ ├── 13 │ │ │ └── stat │ │ └── 14 │ │ │ └── stat │ └── proc_stat ├── psutils_linux_thread_clean_test.lua ├── quantile_test.lua ├── rock_utils.lua ├── tarantool │ ├── config_metrics_test.lua │ ├── cpu_metrics_test.lua │ ├── info_metrics_test.lua │ ├── lj_metrics_test.lua │ ├── memtx_metrics_test.lua │ ├── spaces_test.lua │ └── vinyl_test.lua ├── tarantool3_helpers │ ├── server.lua │ └── treegen.lua ├── unit │ └── cartridge_issues_test.lua ├── utils.lua └── utils_test.lua └── tmp └── .keep /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | [CMakeLists.txt] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [*.cmake] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [*.lua] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | [*.{h,c,cc}] 22 | indent_style = tab 23 | tab_width = 8 24 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | What has been done? Why? What problem is being solved? 2 | 3 | I didn't forget about 4 | 5 | - [ ] Tests 6 | - [ ] Changelog 7 | - [ ] Documentation (README and rst) 8 | - [ ] Rockspec and rpm spec 9 | 10 | Close #??? 11 | -------------------------------------------------------------------------------- /.github/workflows/destroy-deployment.yml: -------------------------------------------------------------------------------- 1 | name: Destroy-deployments 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'doc/**/*' 7 | types: 8 | - closed 9 | jobs: 10 | destroy-deployment: 11 | runs-on: ubuntu-24.04 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | token: "${{ secrets.GITHUB_TOKEN }}" 16 | 17 | - name: Set branch name from source branch 18 | run: echo "BRANCH_NAME=${GITHUB_HEAD_REF##*/}" >> $GITHUB_ENV 19 | 20 | - name: Remove dev server deployment at ${{env.DEPLOYMENT_NAME}} 21 | uses: strumwolf/delete-deployment-environment@v2 22 | with: 23 | token: "${{ secrets.PRIVATE_REPO_ACCESS_TOKEN }}" 24 | environment: "translation-${{env.BRANCH_NAME}}" 25 | -------------------------------------------------------------------------------- /.github/workflows/packaging.yml: -------------------------------------------------------------------------------- 1 | name: packaging 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | # Run not only on tags, otherwise dependent job will skip. 10 | version-check: 11 | # Skip pull request jobs when the source branch is in the same 12 | # repository. 13 | if: | 14 | github.event_name == 'push' || 15 | github.event.pull_request.head.repo.full_name != github.repository 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - name: Check module version 19 | if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }} 20 | uses: tarantool/actions/check-module-version@master 21 | with: 22 | module-name: 'metrics' 23 | version-pre-extraction-hook: | 24 | require('test.rock_utils').assert_nonbuiltin('metrics') 25 | 26 | package: 27 | # Skip pull request jobs when the source branch is in the same 28 | # repository. 29 | if: | 30 | github.event_name == 'push' || 31 | github.event.pull_request.head.repo.full_name != github.repository 32 | runs-on: ubuntu-24.04 33 | needs: version-check 34 | 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | platform: 39 | - { os: 'el', dist: '7' } 40 | - { os: 'el', dist: '8' } 41 | - { os: 'el', dist: '9' } 42 | - { os: 'fedora', dist: '30' } 43 | - { os: 'fedora', dist: '31' } 44 | - { os: 'fedora', dist: '32' } 45 | - { os: 'fedora', dist: '33' } 46 | - { os: 'fedora', dist: '34' } 47 | - { os: 'fedora', dist: '35' } 48 | - { os: 'fedora', dist: '36' } 49 | 50 | env: 51 | OS: ${{ matrix.platform.os }} 52 | DIST: ${{ matrix.platform.dist }} 53 | 54 | steps: 55 | - name: Clone the module 56 | uses: actions/checkout@v4 57 | with: 58 | fetch-depth: 0 59 | 60 | - name: Clone the packpack tool 61 | uses: actions/checkout@v4 62 | with: 63 | repository: packpack/packpack 64 | path: packpack 65 | 66 | - name: Fetch tags 67 | # Found that Github checkout Actions pulls all the tags, but 68 | # right it deannotates the testing tag, check: 69 | # https://github.com/actions/checkout/issues/290 70 | # But we use 'git describe ..' calls w/o '--tags' flag and it 71 | # prevents us from getting the needed tag for packages version 72 | # setup. To avoid of it, let's fetch it manually, to be sure 73 | # that all tags will exist always. 74 | run: git fetch --tags -f 75 | 76 | - name: Create packages 77 | run: ./packpack/packpack 78 | 79 | - name: Deploy packages 80 | # We need this step to run only on push with tag. 81 | if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }} 82 | env: 83 | RWS_URL_PART: https://rws.tarantool.org/tarantool-modules 84 | RWS_AUTH: ${{ secrets.RWS_AUTH }} 85 | PRODUCT_NAME: tarantool-metrics 86 | run: | 87 | CURL_CMD="curl -LfsS \ 88 | -X PUT ${RWS_URL_PART}/${OS}/${DIST} \ 89 | -u ${RWS_AUTH} \ 90 | -F product=${PRODUCT_NAME}" 91 | 92 | for f in $(ls -I '*build*' -I '*.changes' ./build); do 93 | CURL_CMD+=" -F $(basename ${f})=@./build/${f}" 94 | done 95 | 96 | echo ${CURL_CMD} 97 | 98 | ${CURL_CMD} 99 | -------------------------------------------------------------------------------- /.github/workflows/pull-translation.yml: -------------------------------------------------------------------------------- 1 | name: Pull translations 2 | 3 | on: 4 | workflow_dispatch: 5 | branches: 6 | - '!master' 7 | jobs: 8 | pull-translations: 9 | runs-on: ubuntu-24.04 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | token: ${{secrets.PRIVATE_REPO_ACCESS_TOKEN}} 15 | 16 | - name: Set branch name from source branch 17 | run: echo "BRANCH_NAME=${GITHUB_REF##*/}" >> $GITHUB_ENV 18 | 19 | - name: Setup Python environment 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: '3.9' 23 | 24 | - name: Setup Python requirements 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r doc/requirements.txt 28 | 29 | - name: Pull translations from Crowdin 30 | uses: crowdin/github-action@1.0.21 31 | with: 32 | config: 'doc/crowdin.yaml' 33 | upload_sources: false 34 | upload_translations: false 35 | push_translations: false 36 | download_translations: true 37 | download_language: 'ru' 38 | crowdin_branch_name: ${{env.BRANCH_NAME}} 39 | debug_mode: true 40 | env: 41 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 42 | CROWDIN_PERSONAL_TOKEN: ${{secrets.CROWDIN_PERSONAL_TOKEN}} 43 | 44 | - name: Cleanup translation files 45 | run: | 46 | sudo chown -R runner:docker doc/locale/ru/LC_MESSAGES 47 | python doc/cleanup.py po 48 | 49 | - name: Commit translation files 50 | uses: stefanzweifel/git-auto-commit-action@v4.1.2 51 | with: 52 | commit_message: "Update translations" 53 | file_pattern: "*.po" 54 | -------------------------------------------------------------------------------- /.github/workflows/push-translation.yml: -------------------------------------------------------------------------------- 1 | name: Push translation sources 2 | 3 | on: 4 | workflow_dispatch # Temporarily switched off 5 | # pull_request: 6 | # paths: 7 | # - 'doc/**/*' 8 | jobs: 9 | push-translation-sources: 10 | runs-on: ubuntu-24.04 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set branch name from source branch 16 | run: echo "BRANCH_NAME=${GITHUB_HEAD_REF##*/}" >> $GITHUB_ENV 17 | 18 | - name: Start translation service deployment 19 | uses: bobheadxi/deployments@v1.4.0 20 | id: translation 21 | with: 22 | step: start 23 | token: ${{secrets.GITHUB_TOKEN}} 24 | env: translation-${{env.BRANCH_NAME}} 25 | ref: ${{github.head_ref}} 26 | 27 | - name: Setup Python environment 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: '3.9' 31 | - name: Setup Python requirements 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install -r doc/requirements.txt 35 | 36 | - name: Build pot files 37 | run: python -m sphinx . doc/locale/en -c doc -b gettext 38 | 39 | - name: Push POT files to crowdin 40 | uses: crowdin/github-action@1.0.21 41 | with: 42 | upload_sources: true 43 | upload_translations: false 44 | crowdin_branch_name: ${{env.BRANCH_NAME}} 45 | config: 'doc/crowdin.yaml' 46 | env: 47 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 48 | CROWDIN_PERSONAL_TOKEN: ${{secrets.CROWDIN_PERSONAL_TOKEN}} 49 | 50 | - name: update deployment status 51 | uses: bobheadxi/deployments@v1.4.0 52 | with: 53 | step: finish 54 | token: ${{secrets.GITHUB_TOKEN}} 55 | status: ${{job.status}} 56 | deployment_id: ${{steps.translation.outputs.deployment_id}} 57 | env_url: https://crowdin.com/project/tarantool-metrics-docs/ru#/${{env.BRANCH_NAME}} 58 | -------------------------------------------------------------------------------- /.github/workflows/push_rockspec.yml: -------------------------------------------------------------------------------- 1 | name: Push rockspec 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Tests"] 6 | branches: [master] 7 | types: 8 | - completed 9 | push: 10 | tags: 11 | - '*' 12 | 13 | env: 14 | ROCK_NAME: "metrics" 15 | 16 | jobs: 17 | version-check: 18 | # We need this job to run only on push with tag. 19 | if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }} 20 | runs-on: [ ubuntu-24.04 ] 21 | steps: 22 | - name: Check module version 23 | uses: tarantool/actions/check-module-version@master 24 | with: 25 | module-name: 'metrics' 26 | version-pre-extraction-hook: | 27 | require('test.rock_utils').assert_nonbuiltin('metrics') 28 | 29 | push-scm-rockspec: 30 | runs-on: [ ubuntu-24.04 ] 31 | if: github.ref == 'refs/heads/master' && github.event.workflow_run.conclusion == 'success' 32 | steps: 33 | - uses: actions/checkout@master 34 | 35 | - uses: tarantool/rocks.tarantool.org/github-action@master 36 | with: 37 | auth: ${{ secrets.ROCKS_AUTH }} 38 | files: ${{ env.ROCK_NAME }}-scm-1.rockspec 39 | 40 | push-tagged-rockspec: 41 | runs-on: [ ubuntu-24.04 ] 42 | if: startsWith(github.ref, 'refs/tags') 43 | needs: version-check 44 | steps: 45 | - uses: actions/checkout@master 46 | 47 | # https://stackoverflow.com/questions/58177786/get-the-current-pushed-tag-in-github-actions 48 | - name: Set env 49 | run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 50 | 51 | - name: Push release rockspec 52 | run: | 53 | sed \ 54 | -e "s/branch = '.\+'/tag = '${GIT_TAG}'/g" \ 55 | -e "s/version = '.\+'/version = '${GIT_TAG}-1'/g" \ 56 | ${{ env.ROCK_NAME }}-scm-1.rockspec > ${{ env.ROCK_NAME }}-${GIT_TAG}-1.rockspec 57 | 58 | - uses: tarantool/rocks.tarantool.org/github-action@master 59 | with: 60 | auth: ${{ secrets.ROCKS_AUTH }} 61 | files: ${{ env.ROCK_NAME }}-${GIT_TAG}-1.rockspec 62 | -------------------------------------------------------------------------------- /.github/workflows/reusable-test.yml: -------------------------------------------------------------------------------- 1 | name: Reusable Test 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | artifact_name: 7 | description: 'The name of the tarantool build artifact' 8 | default: ubuntu-noble 9 | required: false 10 | type: string 11 | 12 | jobs: 13 | run_tests: 14 | runs-on: ubuntu-24.04 15 | steps: 16 | - name: 'Clone the metrics module' 17 | uses: actions/checkout@v4 18 | with: 19 | repository: ${{ github.repository_owner }}/metrics 20 | 21 | - name: 'Download the tarantool build artifact' 22 | uses: actions/download-artifact@v4 23 | with: 24 | name: ${{ inputs.artifact_name }} 25 | 26 | - name: 'Install tarantool' 27 | # Now we're lucky: all dependencies are already installed. Check package 28 | # dependencies when migrating to other OS version. 29 | run: sudo dpkg -i tarantool*.deb 30 | 31 | - name: Setup tt 32 | run: | 33 | curl -L https://tarantool.io/release/2/installer.sh | sudo bash 34 | sudo apt install -y tt 35 | tt version 36 | 37 | - run: make test 38 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues" 2 | on: 3 | schedule: 4 | - cron: "0 18 * * *" 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-24.04 9 | steps: 10 | - uses: actions/stale@v3 11 | with: 12 | repo-token: ${{ secrets.GITHUB_TOKEN }} 13 | stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 30 days' 14 | stale-pr-message: 'This pull request is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 30 days' 15 | days-before-stale: 60 16 | days-before-close: 30 17 | exempt-issue-labels: bug,feature,customer 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: 5 | - '**' 6 | paths-ignore: 7 | - 'doc/**' 8 | jobs: 9 | test: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | include: 14 | # 2.11 15 | - tarantool: "2.11" 16 | cartridge: "2.7.9" 17 | - tarantool: "2.11" 18 | cartridge: "2.8.6" 19 | - tarantool: "2.11" 20 | cartridge: "2.9.0" 21 | - tarantool: "2.11" 22 | cartridge: "2.10.0" 23 | - tarantool: "2.11" 24 | cartridge: "2.11.0" 25 | - tarantool: "2.11" 26 | cartridge: "2.12.4" 27 | - tarantool: "2.11" 28 | cartridge: "2.13.0" 29 | - tarantool: "2.11" 30 | cartridge: "" 31 | # 3.x 32 | - tarantool: "3.0" 33 | cartridge: "" 34 | - tarantool: "3.1" 35 | cartridge: "" 36 | - tarantool: "3.2" 37 | cartridge: "" 38 | - tarantool: "3.3" 39 | cartridge: "" 40 | runs-on: ubuntu-24.04 41 | steps: 42 | - uses: actions/checkout@v4 43 | 44 | - uses: tarantool/setup-tarantool@v3 45 | with: 46 | tarantool-version: ${{ matrix.tarantool }} 47 | 48 | # Stop Mono server. This server starts and listens to 8084 port that is 49 | # used for tests. 50 | - name: 'Stop Mono server' 51 | run: sudo kill -9 $(sudo lsof -t -i tcp:8084) || true 52 | 53 | - name: Setup tt 54 | run: | 55 | curl -L https://tarantool.io/release/2/installer.sh | sudo bash 56 | sudo apt install -y tt 57 | tt version 58 | 59 | - name: lint 60 | run: make lint 61 | env: 62 | CARTRIDGE_VERSION: ${{ matrix.cartridge }} 63 | 64 | - name: test 65 | run: make test_with_coverage_report 66 | 67 | packpack: 68 | runs-on: ubuntu-24.04 69 | needs: test 70 | steps: 71 | - uses: actions/checkout@v4 72 | 73 | - name: packpack 74 | run: | 75 | git clone https://github.com/packpack/packpack.git packpack 76 | OS=el DIST=8 packpack/packpack 77 | ls -l build/ 78 | 79 | promtool: 80 | runs-on: ubuntu-24.04 81 | strategy: 82 | matrix: 83 | tarantool: 84 | - "2.11" 85 | cartridge: 86 | - "2.10.0" 87 | include: 88 | - tarantool: "3.1" 89 | cartridge: "" 90 | needs: test 91 | steps: 92 | - uses: actions/checkout@v4 93 | 94 | - uses: tarantool/setup-tarantool@v3 95 | with: 96 | tarantool-version: ${{ matrix.tarantool }} 97 | 98 | - name: Setup tt 99 | run: | 100 | curl -L https://tarantool.io/release/2/installer.sh | sudo bash 101 | sudo apt install -y tt 102 | tt version 103 | 104 | - uses: actions/setup-go@v5 105 | with: 106 | go-version: '1.15' 107 | 108 | - name: promtool test 109 | env: 110 | CARTRIDGE_VERSION: ${{ matrix.cartridge }} 111 | run: | 112 | GO111MODULE=on go get github.com/prometheus/prometheus/cmd/promtool@a6be548dbc17780d562a39c0e4bd0bd4c00ad6e2 113 | make test_promtool 114 | -------------------------------------------------------------------------------- /.github/workflows/upload-translations.yml: -------------------------------------------------------------------------------- 1 | name: Update translations on the main branch 2 | 3 | on: 4 | workflow_dispatch # Temporarily switched off 5 | # push: 6 | # paths: 7 | # - 'doc/**/*' 8 | # branches: 9 | # - master 10 | jobs: 11 | autocommit-pot-files: 12 | runs-on: ubuntu-24.04 13 | 14 | steps: 15 | 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Setup Python environment 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: '3.9' 23 | 24 | - name: Setup Python requirements 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r doc/requirements.txt 28 | 29 | - name: Build pot files 30 | run: python -m sphinx . doc/locale/en -c doc -b gettext 31 | 32 | - name: Push Pot-files to crowdin 33 | uses: crowdin/github-action@1.1.0 34 | with: 35 | config: 'doc/crowdin.yaml' 36 | upload_sources: true 37 | upload_translations: true 38 | import_eq_suggestions: true 39 | env: 40 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 41 | CROWDIN_PERSONAL_TOKEN: ${{secrets.CROWDIN_PERSONAL_TOKEN}} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | 513 3 | .rocks 4 | *.snap 5 | *.xlog 6 | *.vylog 7 | 8 | /tmp/* 9 | !/tmp/.keep 10 | .history 11 | .vscode 12 | prometheus-input 13 | doc/.doctrees/ 14 | doc/output/ 15 | doc/locale/en/ 16 | 17 | *.lua.c 18 | 19 | build.luarocks 20 | packpack 21 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/metrics/7f584b94f59c45d2d83907ab96cc2be7c3b83d16/.gitmodules -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | include_files = {"**/*.lua", "*.rockspec", "*.luacheckrc"} 2 | exclude_files = {"lua_modules/", ".luarocks/", ".rocks/", "tmp/", ".history/"} 3 | 4 | max_line_length = 120 5 | max_comment_line_length = 200 6 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | statsfile = 'tmp/luacov.stats.out' 2 | reportfile = 'tmp/luacov.report.out' 3 | exclude = { 4 | '/test/.+_test', 5 | '/tmp/', 6 | } 7 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5 FATAL_ERROR) 2 | 3 | project(metrics NONE) 4 | 5 | ## Install #################################################################### 6 | ############################################################################### 7 | 8 | if(NOT DEFINED TARANTOOL_INSTALL_LUADIR) 9 | set(TARANTOOL_INSTALL_LUADIR "${PROJECT_SOURCE_DIR}/.rocks/share/tarantool") 10 | endif() 11 | 12 | install( 13 | DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME} 14 | DESTINATION ${TARANTOOL_INSTALL_LUADIR} 15 | ) 16 | 17 | install( 18 | DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME} 19 | DESTINATION ${TARANTOOL_INSTALL_LUADIR}/override 20 | ) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018-2023 Tarantool 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TTCTL := tt 2 | ifeq (,$(shell which tt 2>/dev/null)) 3 | $(error tt is not found) 4 | endif 5 | 6 | .PHONY: rpm 7 | rpm: 8 | OS=el DIST=7 packpack/packpack 9 | 10 | .rocks: metrics-scm-1.rockspec metrics/*.lua metrics/*/*.lua 11 | $(TTCTL) rocks make 12 | $(TTCTL) rocks install luatest 1.0.1 13 | $(TTCTL) rocks install luacov 0.13.0 14 | $(TTCTL) rocks install luacheck 0.26.0 15 | if [ -n '$(CARTRIDGE_VERSION)' ]; then \ 16 | $(TTCTL) rocks install cartridge $(CARTRIDGE_VERSION); \ 17 | fi 18 | 19 | .PHONY: lint 20 | lint: .rocks 21 | .rocks/bin/luacheck . 22 | 23 | .PHONY: test 24 | test: .rocks 25 | .rocks/bin/luatest -v -c 26 | 27 | .PHONY: test_with_coverage_report 28 | test_with_coverage_report: .rocks 29 | rm -f tmp/luacov.*.out* 30 | .rocks/bin/luatest --coverage -v -c --shuffle group --repeat 3 31 | .rocks/bin/luacov . 32 | echo 33 | grep -A999 '^Summary' tmp/luacov.report.out 34 | 35 | .PHONY: test_promtool 36 | test_promtool: .rocks 37 | tarantool test/promtool.lua 38 | cat prometheus-input | promtool check metrics 39 | rm prometheus-input 40 | 41 | update-pot: 42 | sphinx-build doc/monitoring doc/locale/en/ -c doc/ -d doc/.doctrees -b gettext 43 | 44 | update-po: 45 | sphinx-intl update -p doc/locale/en/ -d doc/locale/ -l "ru" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf .rocks 50 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | [![Crowdin](https://badges.crowdin.net/tarantool-metrics-docs/localized.svg)](https://crowdin.com/project/tarantool-metrics-docs) 2 | # Tarantool Metrics documentation 3 | Part of Tarantool documentation, published to 4 | https://www.tarantool.io/en/doc/latest/book/monitoring/ 5 | 6 | ## Create pot files from rst 7 | ```bash 8 | python -m sphinx doc doc/locale/en -c doc -b gettext 9 | ``` 10 | 11 | ## Create/update po from pot files 12 | ```bash 13 | sphinx-intl update -p doc/locale/en -d doc/locale -l ru 14 | ``` 15 | 16 | ## Build documentation to doc/output 17 | ```bash 18 | python -m sphinx doc doc/output -c doc 19 | ``` 20 | -------------------------------------------------------------------------------- /doc/cleanup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | import argparse 3 | from glob import glob 4 | from polib import pofile, POFile, _BaseFile 5 | 6 | parser = argparse.ArgumentParser(description='Cleanup PO and POT files') 7 | parser.add_argument('extension', type=str, choices=['po', 'pot', 'both'], 8 | help='cleanup files with extension: po, pot or both') 9 | 10 | 11 | class PoFile(POFile): 12 | 13 | def __unicode__(self): 14 | return _BaseFile.__unicode__(self) 15 | 16 | def metadata_as_entry(self): 17 | class M: 18 | def __unicode__(self, _): 19 | return '' 20 | return M() 21 | 22 | 23 | def cleanup_files(extension): 24 | mask = f'**/*.{extension}' 25 | for file_path in glob(mask, recursive=True): 26 | print(f'cleanup {file_path}') 27 | po_file: POFile = pofile(file_path, klass=PoFile) 28 | po_file.header = '' 29 | po_file.metadata = {} 30 | po_file.metadata_is_fuzzy = False 31 | 32 | for item in po_file: 33 | item.occurrences = None 34 | 35 | po_file.save() 36 | 37 | 38 | if __name__ == "__main__": 39 | 40 | args = parser.parse_args() 41 | 42 | if args.extension in ['po', 'both']: 43 | cleanup_files('po') 44 | 45 | if args.extension in ['pot', 'both']: 46 | cleanup_files('pot') 47 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | sys.path.insert(0, os.path.abspath('..')) 5 | 6 | master_doc = 'doc/monitoring/index' 7 | 8 | source_suffix = '.rst' 9 | 10 | project = u'Monitoring' 11 | 12 | exclude_patterns = [ 13 | 'conf.py', 14 | 'monitoring/images', 15 | 'requirements.txt', 16 | 'locale', 17 | ] 18 | 19 | language = 'en' 20 | locale_dirs = ['./locale'] 21 | gettext_compact = False 22 | gettext_location = False 23 | -------------------------------------------------------------------------------- /doc/crowdin.yaml: -------------------------------------------------------------------------------- 1 | # https://support.crowdin.com/configuration-file/ 2 | # https://support.crowdin.com/cli-tool-v3/#configuration 3 | 4 | "project_id" : "447876" 5 | "base_path" : "doc/locale" 6 | "base_url": "https://crowdin.com" 7 | "api_token_env": "CROWDIN_PERSONAL_TOKEN" 8 | 9 | 10 | "preserve_hierarchy": true 11 | 12 | files: [ 13 | { 14 | "source" : "/en/**/*.pot", 15 | "translation" : "/%locale_with_underscore%/LC_MESSAGES/**/%file_name%.po", 16 | "update_option" : "update_as_unapproved", 17 | 18 | "languages_mapping" : { 19 | "locale_with_underscore" : { 20 | "ru" : "ru", 21 | } 22 | }, 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /doc/locale/ru/LC_MESSAGES/getting_started.po: -------------------------------------------------------------------------------- 1 | 2 | msgid "Monitoring: getting started" 3 | msgstr "" 4 | 5 | msgid "Tarantool" 6 | msgstr "" 7 | 8 | msgid "First, you need to install the ``metrics`` package:" 9 | msgstr "" 10 | 11 | msgid "Next, require it in your code:" 12 | msgstr "" 13 | 14 | msgid "Set a global label for your metrics:" 15 | msgstr "" 16 | 17 | msgid "" 18 | "Enable default Tarantool metrics such as network, memory, operations, etc:" 19 | msgstr "" 20 | 21 | msgid "If you use Cartridge, enable Cartridge metrics:" 22 | msgstr "" 23 | 24 | msgid "" 25 | "Initialize the Prometheus Exporter, or export metrics in any other format:" 26 | msgstr "" 27 | 28 | msgid "" 29 | "Now you can use the HTTP API endpoint ``/metrics`` to collect your metrics " 30 | "in the Prometheus format. If you need your custom metrics, see the :ref:`API" 31 | " reference `." 32 | msgstr "" 33 | 34 | msgid "Instance health check" 35 | msgstr "" 36 | 37 | msgid "" 38 | "In production environments Tarantool Cluster usually has a large number of " 39 | "so called \"routers\", Tarantool instances that handle input load and it is " 40 | "required to evenly distribute the load. Various load-balancers are used for " 41 | "this, but any load-balancer have to know which \"routers\" are ready to " 42 | "accept the load at that very moment. Metrics library has a special plugin " 43 | "that creates an http handler that can be used by the load-balancer to check " 44 | "the current state of any Tarantool instance. If the instance is ready to " 45 | "accept the load, it will return a response with a 200 status code, if not, " 46 | "with a 500 status code." 47 | msgstr "" 48 | 49 | msgid "Cartridge role" 50 | msgstr "" 51 | 52 | msgid "" 53 | "``cartridge.roles.metrics`` is a role for `Tarantool Cartridge " 54 | "`_. It allows using default metrics " 55 | "in a Cartridge application and manage them via configuration." 56 | msgstr "" 57 | 58 | msgid "**Usage**" 59 | msgstr "" 60 | 61 | msgid "" 62 | "Add ``metrics`` package to dependencies in the ``.rockspec`` file. Make sure" 63 | " that you are using version **0.3.0** or higher." 64 | msgstr "" 65 | 66 | msgid "" 67 | "Make sure that you have ``cartridge.roles.metrics`` in the roles list in " 68 | "``cartridge.cfg`` in your entry-point file (e.g. ``init.lua``)." 69 | msgstr "" 70 | 71 | msgid "To view metrics via API endpoints, use ``set_export``." 72 | msgstr "" 73 | 74 | msgid "" 75 | "**NOTE** that ``set_export`` has lower priority than clusterwide config and " 76 | "won't work if metrics config is present." 77 | msgstr "" 78 | 79 | msgid "" 80 | "You can add several entry points of the same format by different paths, like" 81 | " this:" 82 | msgstr "" 83 | 84 | msgid "" 85 | "The metrics will be available on the path specified in ``path`` in the " 86 | "format specified in ``format``." 87 | msgstr "" 88 | 89 | msgid "Enable role in the interface:" 90 | msgstr "" 91 | 92 | msgid "" 93 | "Since version **0.6.0** metrics role is permanent and enabled on instances " 94 | "by default." 95 | msgstr "" 96 | 97 | msgid "" 98 | "After role initialization, default metrics will be enabled and the global " 99 | "label ``'alias'`` will be set. **Note** that ``'alias'`` label value is set " 100 | "by instance :ref:`configuration option ` ``alias`` or " 101 | "``instance_name`` (since **0.6.1**)." 102 | msgstr "" 103 | 104 | msgid "" 105 | "If you need to use the functionality of any metrics package, you may get it " 106 | "as a Cartridge service and use it like a regular package after ``require``:" 107 | msgstr "" 108 | 109 | msgid "" 110 | "There is an ability in Tarantool Cartridge >= ``'2.4.0'`` to set a zone for " 111 | "each server in cluster. If zone was set for the server ``'zone'`` label for " 112 | "all metrics of this server will be added." 113 | msgstr "" 114 | 115 | msgid "" 116 | "To change metrics HTTP path in **runtime**, you may use the following " 117 | "configuration (to learn more about Cartridge configuration, see `this " 118 | "`_). We don't recommend to use it to set" 120 | " up metrics role, use ``set_export`` instead." 121 | msgstr "" 122 | -------------------------------------------------------------------------------- /doc/locale/ru/LC_MESSAGES/index.po: -------------------------------------------------------------------------------- 1 | 2 | msgid "Monitoring" 3 | msgstr "" 4 | 5 | msgid "" 6 | "Monitoring is the process of measuring and tracking Tarantool performance " 7 | "according to key metrics influencing it. These metrics are typically " 8 | "monitored in real time, allowing you to identify or predict issues." 9 | msgstr "" 10 | 11 | msgid "This chapter includes the following sections:" 12 | msgstr "" 13 | -------------------------------------------------------------------------------- /doc/locale/ru/LC_MESSAGES/plugins.po: -------------------------------------------------------------------------------- 1 | 2 | msgid "Metrics plugins" 3 | msgstr "" 4 | 5 | msgid "" 6 | "Plugins allow using a unified interface to collect metrics without worrying " 7 | "about the way metrics export is performed. If you want to use another DB to " 8 | "store metrics data, you can use an appropriate export plugin just by " 9 | "changing one line of code." 10 | msgstr "" 11 | 12 | msgid "Available plugins" 13 | msgstr "" 14 | 15 | msgid "Prometheus" 16 | msgstr "" 17 | 18 | msgid "**Usage**" 19 | msgstr "" 20 | 21 | msgid "Import the Prometheus plugin:" 22 | msgstr "" 23 | 24 | msgid "" 25 | "Further, use the ``prometheus.collect_http()`` function, which returns:" 26 | msgstr "" 27 | 28 | msgid "" 29 | "See the `Prometheus exposition format " 30 | "`_" 31 | " for details on ```` and ````." 32 | msgstr "" 33 | 34 | msgid "" 35 | "Use in Tarantool `http.server `_ as " 36 | "follows:" 37 | msgstr "" 38 | 39 | msgid "" 40 | "In Tarantool `http.server v1 " 41 | "`_ (currently used in " 42 | "`Tarantool Cartridge `_):" 43 | msgstr "" 44 | 45 | msgid "" 46 | "In Tarantool `http.server v2 `_ (the " 47 | "latest version):" 48 | msgstr "" 49 | 50 | msgid "**Sample settings**" 51 | msgstr "" 52 | 53 | msgid "For Tarantool ``http.server`` v1:" 54 | msgstr "" 55 | 56 | msgid "For Tarantool Cartridge (with ``http.server`` v1):" 57 | msgstr "" 58 | 59 | msgid "For Tarantool ``http.server`` v2:" 60 | msgstr "" 61 | 62 | msgid "Graphite" 63 | msgstr "" 64 | 65 | msgid "Import the Graphite plugin:" 66 | msgstr "" 67 | 68 | msgid "" 69 | "To start automatically exporting the current values of all " 70 | "``metrics.{counter,gauge,histogram}``, just call:" 71 | msgstr "" 72 | 73 | msgid "Parameters" 74 | msgstr "" 75 | 76 | msgid "" 77 | "Possible options: * ``prefix`` (string) - metrics prefix (default is " 78 | "``'tarantool'``); * ``host`` (string) - graphite server host (default is " 79 | "``'127.0.0.1'``); * ``port`` (number) - graphite server port (default is " 80 | "``2003``); * ``send_interval`` (number) - metrics collect interval in " 81 | "seconds (default is ``2``);" 82 | msgstr "" 83 | 84 | msgid "Possible options:" 85 | msgstr "" 86 | 87 | msgid "``prefix`` (string) - metrics prefix (default is ``'tarantool'``);" 88 | msgstr "" 89 | 90 | msgid "``host`` (string) - graphite server host (default is ``'127.0.0.1'``);" 91 | msgstr "" 92 | 93 | msgid "``port`` (number) - graphite server port (default is ``2003``);" 94 | msgstr "" 95 | 96 | msgid "" 97 | "``send_interval`` (number) - metrics collect interval in seconds (default is" 98 | " ``2``);" 99 | msgstr "" 100 | 101 | msgid "" 102 | "This creates a background fiber that periodically sends all metrics to a " 103 | "remote Graphite server." 104 | msgstr "" 105 | 106 | msgid "Exported metric name is sent in the format ``.``." 107 | msgstr "" 108 | 109 | msgid "JSON" 110 | msgstr "" 111 | 112 | msgid "Import the JSON plugin:" 113 | msgstr "" 114 | 115 | msgid "Returns" 116 | msgstr "" 117 | 118 | msgid "" 119 | "the following structure .. code-block:: json [ { " 120 | "\"name\": \"\", \"label_pairs\": { " 121 | "\"\": \"\", \"...\": \"...\" }," 122 | " \"timestamp\": \"\", \"value\": \"\"" 123 | " }, \"...\" ]" 124 | msgstr "" 125 | 126 | msgid "the following structure" 127 | msgstr "" 128 | 129 | msgid "Return type" 130 | msgstr "" 131 | 132 | msgid "Values can be ``+-math.huge``, ``math.huge * 0``. Then:" 133 | msgstr "" 134 | 135 | msgid "``math.inf`` is serialized to ``\"inf\"``" 136 | msgstr "" 137 | 138 | msgid "``-math.inf`` is serialized to ``\"-inf\"``" 139 | msgstr "" 140 | 141 | msgid "``nan`` is serialized to ``\"nan\"``" 142 | msgstr "" 143 | 144 | msgid "**Example**" 145 | msgstr "" 146 | 147 | msgid "To be used in Tarantool ``http.server`` as follows:" 148 | msgstr "" 149 | 150 | msgid "Plugin-specific API" 151 | msgstr "" 152 | 153 | msgid "" 154 | "We encourage you to use the following methods **only when developing a new " 155 | "plugin**." 156 | msgstr "" 157 | 158 | msgid "" 159 | "Invokes the function registered via " 160 | "``metrics.register_callback()``. Used in exporters." 161 | msgstr "" 162 | 163 | msgid "Designed to be used in exporters in favor of ``metrics.collect()``." 164 | msgstr "" 165 | 166 | msgid "a list of created collectors" 167 | msgstr "" 168 | 169 | msgid "You'll probably want to use ``metrics.collectors()`` instead." 170 | msgstr "" 171 | 172 | msgid "Equivalent to:" 173 | msgstr "" 174 | 175 | msgid "" 176 | "Concatenation of ``observation`` objects across all created collectors." 177 | " .. code-block:: lua { label_pairs: table, -- " 178 | "`label_pairs` key-value table timestamp: ctype, -- current" 179 | " system time (in microseconds) value: number, -- " 180 | "current value metric_name: string, -- collector }" 181 | msgstr "" 182 | 183 | msgid "Concatenation of ``observation`` objects across all" 184 | msgstr "" 185 | 186 | msgid "created collectors." 187 | msgstr "" 188 | 189 | msgid "Writing custom plugins" 190 | msgstr "" 191 | 192 | msgid "Inside your main export function:" 193 | msgstr "" 194 | -------------------------------------------------------------------------------- /doc/monitoring/images/histogram-buckets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/metrics/7f584b94f59c45d2d83907ab96cc2be7c3b83d16/doc/monitoring/images/histogram-buckets.png -------------------------------------------------------------------------------- /doc/monitoring/images/histogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/metrics/7f584b94f59c45d2d83907ab96cc2be7c3b83d16/doc/monitoring/images/histogram.png -------------------------------------------------------------------------------- /doc/monitoring/images/role-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/metrics/7f584b94f59c45d2d83907ab96cc2be7c3b83d16/doc/monitoring/images/role-config.png -------------------------------------------------------------------------------- /doc/monitoring/images/role-enable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/metrics/7f584b94f59c45d2d83907ab96cc2be7c3b83d16/doc/monitoring/images/role-enable.png -------------------------------------------------------------------------------- /doc/monitoring/images/summary-buckets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/metrics/7f584b94f59c45d2d83907ab96cc2be7c3b83d16/doc/monitoring/images/summary-buckets.png -------------------------------------------------------------------------------- /doc/monitoring/index.rst: -------------------------------------------------------------------------------- 1 | .. _monitoring: 2 | 3 | Monitoring 4 | ========== 5 | 6 | Monitoring is the process of measuring and tracking Tarantool performance 7 | based on metrics. The metrics are typically monitored 8 | in real time, which allows you to identify or predict issues. 9 | 10 | This chapter includes the following sections: 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | :numbered: 0 15 | 16 | getting_started 17 | install 18 | getting_started_cartridge 19 | metrics_reference 20 | api_reference 21 | plugins 22 | grafana_dashboard 23 | alerting 24 | -------------------------------------------------------------------------------- /doc/monitoring/install.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | Installing the metrics module 4 | ============================= 5 | 6 | .. note:: 7 | 8 | Since Tarantool version `2.11.1 `__, 9 | the installation is not required. 10 | 11 | .. _install-rockspec: 12 | 13 | Installing metrics using the .rockspec file 14 | ------------------------------------------- 15 | 16 | Usually, all dependencies are included in the ``*.rockspec`` file of the application. 17 | All dependencies are installed from this file. To do this: 18 | 19 | #. Add the ``metrics`` module to the dependencies in the ``.rockspec`` file: 20 | 21 | .. code-block:: lua 22 | 23 | dependencies = { 24 | ... 25 | 'metrics == 1.0.0', 26 | ... 27 | } 28 | 29 | #. Install the missing dependencies: 30 | 31 | .. code-block:: shell 32 | 33 | tt rocks make 34 | # OR # 35 | cartridge build 36 | 37 | .. _install-metrics_only: 38 | 39 | Installing the metrics module only 40 | ---------------------------------- 41 | 42 | To install only the ``metrics`` module, execute the following commands: 43 | 44 | #. Set current folder: 45 | 46 | .. code-block:: shell 47 | 48 | $ cd ${PROJECT_ROOT} 49 | 50 | #. Install the missing dependencies: 51 | 52 | .. code-block:: shell 53 | 54 | $ tt rocks install metrics 55 | 56 | where ``version`` -- the necessary version number. If omitted, then the version from the 57 | ``master`` branch is installed. 58 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==4.0.2 2 | sphinx-intl==2.0.1 3 | polib==1.1.1 4 | -------------------------------------------------------------------------------- /example/HTTP/latency_v1.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | package.path = package.path .. ";../?.lua" 3 | 4 | local json = require('json') 5 | local fiber = require('fiber') 6 | local metrics = require('metrics') 7 | local log = require('log') 8 | local http_middleware = metrics.http_middleware 9 | 10 | -- Configure HTTP routing 11 | local ip = '127.0.0.1' 12 | local port = 12345 13 | local httpd = require('http.server').new(ip, port) -- HTTP ver. 1.x.x 14 | local route = { path = '/path', method = 'POST' } 15 | 16 | -- Route handler 17 | local handler = function(req) 18 | for _ = 1, 10 do 19 | fiber.sleep(0.1) 20 | end 21 | 22 | return { status = 200, body = req.body } 23 | end 24 | 25 | -- Configure summary latency collector 26 | local collector = http_middleware.build_default_collector('summary') 27 | 28 | -- Set route handler with summary latency collection 29 | httpd:route(route, http_middleware.v1(handler, collector)) 30 | -- Start HTTP routing 31 | httpd:start() 32 | 33 | -- Set HTTP client, make some request 34 | local http_client = require("http.client") -- HTTP ver. 1.x.x 35 | http_client.post('http://' .. ip .. ':' .. port .. route.path, json.encode({ body = 'text' })) 36 | 37 | -- Collect the metrics 38 | log.info(metrics.collect()) 39 | --[[ 40 | 41 | - label_pairs: 42 | path: /path 43 | method: POST 44 | status: 200 45 | timestamp: 1588951616500768 46 | value: 1 47 | metric_name: path_latency_count 48 | 49 | - label_pairs: 50 | path: /path 51 | method: POST 52 | status: 200 53 | timestamp: 1588951616500768 54 | value: 1.0240110000595 55 | metric_name: path_latency_sum 56 | 57 | - label_pairs: 58 | path: /path 59 | method: POST 60 | status: 200 61 | quantile: 0.5 62 | timestamp: 1588951616500768 63 | value: 1.0240110000595 64 | metric_name: path_latency 65 | 66 | - label_pairs: 67 | path: /path 68 | method: POST 69 | status: 200 70 | quantile: 0.9 71 | timestamp: 1588951616500768 72 | value: 1.0240110000595 73 | metric_name: path_latency 74 | 75 | - label_pairs: 76 | path: /path 77 | method: POST 78 | status: 200 79 | quantile: 0.99 80 | timestamp: 1588951616500768 81 | value: 1.0240110000595 82 | metric_name: path_latency 83 | 84 | --]] 85 | 86 | -- Exit event loop 87 | os.exit() 88 | -------------------------------------------------------------------------------- /example/default_metrics.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | package.path = package.path .. ";../?.lua" 3 | 4 | local log = require('log') 5 | 6 | -- Create a Metrics Client 7 | local metrics = require('metrics') 8 | 9 | -- Enable default metrics collections 10 | 11 | metrics.enable_default_metrics(); 12 | 13 | 14 | -- Init Prometheus Exporter 15 | local httpd = require('http.server') 16 | local http_handler = require('metrics.plugins.prometheus').collect_http 17 | 18 | 19 | httpd.new('0.0.0.0', 8088) 20 | :route({path = '/metrics'}, function(...) 21 | log.info('---------------------') 22 | log.info('Handling GET /metrics') 23 | return http_handler(...) 24 | end) 25 | :start() 26 | 27 | box.cfg{ 28 | listen = 3302 29 | } 30 | -------------------------------------------------------------------------------- /example/graphite_export.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | package.path = package.path .. ";../?.lua" 3 | 4 | -- Create a Metrics Client 5 | local metrics = require('metrics') 6 | 7 | -- Init Graphite Exporter 8 | local graphite = require('metrics.plugins.graphite') 9 | graphite.init{ 10 | prefix = 'tarantool', 11 | host = '127.0.0.1', 12 | port = 2003, 13 | send_interval = 1, 14 | } -- Now started background worker which will collect all values of 15 | -- metrics.{counter,gauge,histogram} created below once per second and send them to 16 | -- 127.0.0.1:2003 in graphite format 17 | 18 | -- Create Collectors 19 | local http_requests_total_counter = metrics.counter('http_requests_total') 20 | local cpu_usage_gauge = metrics.gauge('cpu_usage') 21 | local http_requests_total_hist = metrics.histogram('http_requests_total', nil, {2, 4, 6}) 22 | 23 | -- Use Collectors 24 | http_requests_total_counter:inc(1, {method = 'GET'}) 25 | cpu_usage_gauge:set(0.24, {app = 'tarantool'}) 26 | http_requests_total_hist:observe(1) 27 | 28 | -- Register Callbacks 29 | metrics.register_callback(function() 30 | cpu_usage_gauge:set(math.random(), {app = 'tarantool'}) 31 | end) 32 | metrics.register_callback(function() 33 | http_requests_total_counter:inc(1, {method = 'POST'}) 34 | http_requests_total_hist:observe(math.random(1, 10)) 35 | end) -- this functions will be automatically called before every metrics.collect() 36 | -------------------------------------------------------------------------------- /example/latency_observer.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | package.path = package.path .. ";../?.lua" 3 | 4 | local fiber = require('fiber') 5 | local metrics = require('metrics') 6 | 7 | box.cfg{ 8 | listen = 3302 9 | } 10 | 11 | box.schema.user.grant( 12 | 'guest', 'read,write,execute', 'universe', nil, {if_not_exists = true} 13 | ) 14 | 15 | -- Measured function 16 | local function worker(time) 17 | fiber.sleep(time) 18 | return true 19 | end 20 | 21 | -- Create histogram 22 | local latency_hist = metrics.histogram('func_call_latency', 'Help message', {0.1, 0.5, 0.9}) 23 | 24 | -- Wrapper with observe_latency 25 | local function wrapper_worker(...) 26 | return latency_hist:observe_latency( 27 | -- Dynamic label pairs, depends on success 28 | function(ok, _, _) return {ok = tostring(ok)} end, 29 | -- Wrapped function 30 | worker, 31 | -- Function args 32 | ... 33 | ) 34 | end 35 | 36 | rawset(_G, 'api_function', wrapper_worker) 37 | -------------------------------------------------------------------------------- /example/prometheus_export.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | package.path = package.path .. ";../?.lua" 3 | 4 | local log = require('log') 5 | 6 | -- Create a Metrics Client 7 | local metrics = require('metrics') 8 | 9 | -- Init Prometheus Exporter 10 | local httpd = require('http.server') 11 | local http_handler = require('metrics.plugins.prometheus').collect_http 12 | httpd.new('0.0.0.0', 8088) 13 | :route({path = '/metrics'}, function(...) 14 | log.info('---------------------') 15 | log.info('Handling GET /metrics') 16 | return http_handler(...) 17 | end) 18 | :start() 19 | 20 | -- Create Collectors 21 | local http_requests_total_counter = metrics.counter('http_requests_total') 22 | local cpu_usage_gauge = metrics.gauge('cpu_usage') 23 | local http_requests_total_hist = metrics.histogram('http_requests_total', nil, {2, 4, 6}) 24 | 25 | -- Use Collectors 26 | http_requests_total_counter:inc(1, {method = 'GET'}) 27 | cpu_usage_gauge:set(0.24, {app = 'tarantool'}) 28 | http_requests_total_hist:observe(1) 29 | 30 | -- Register Callbacks 31 | metrics.register_callback(function() 32 | cpu_usage_gauge:set(math.random(), {app = 'tarantool'}) 33 | end) 34 | 35 | metrics.register_callback(function() 36 | http_requests_total_counter:inc(1, {method = 'POST'}) 37 | http_requests_total_hist:observe(math.random(1, 10)) 38 | end) -- this functions will be automatically called before every metrics.collect() 39 | -------------------------------------------------------------------------------- /metrics-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = 'metrics' 2 | version = 'scm-1' 3 | 4 | source = { 5 | url = 'git+https://github.com/tarantool/metrics.git', 6 | branch = 'master' 7 | } 8 | 9 | description = { 10 | summary = "A centralized system for collecting and manipulating metrics from multiple clients", 11 | homepage = '', 12 | license = 'BSD', 13 | maintainer = "Albert Sverdlov "; 14 | } 15 | 16 | dependencies = { 17 | 'lua >= 5.1', 18 | 'checks >= 2.0.0', 19 | } 20 | 21 | build = { 22 | type = 'cmake', 23 | variables = { 24 | TARANTOOL_INSTALL_LUADIR = '$(LUADIR)', 25 | }, 26 | } 27 | 28 | -- vim: syntax=lua 29 | -------------------------------------------------------------------------------- /metrics/api.lua: -------------------------------------------------------------------------------- 1 | -- vim: ts=4:sw=4:sts=4:expandtab 2 | 3 | local checks = require('checks') 4 | 5 | local Registry = require('metrics.registry') 6 | 7 | local Counter = require('metrics.collectors.counter') 8 | local Gauge = require('metrics.collectors.gauge') 9 | local Histogram = require('metrics.collectors.histogram') 10 | local Summary = require('metrics.collectors.summary') 11 | 12 | local registry = rawget(_G, '__metrics_registry') 13 | if not registry then 14 | registry = Registry.new() 15 | end 16 | registry.callbacks = {} 17 | 18 | rawset(_G, '__metrics_registry', registry) 19 | 20 | local function collectors() 21 | return registry.collectors 22 | end 23 | 24 | local function register_callback(...) 25 | return registry:register_callback(...) 26 | end 27 | 28 | local function unregister_callback(...) 29 | return registry:unregister_callback(...) 30 | end 31 | 32 | local function invoke_callbacks() 33 | return registry:invoke_callbacks() 34 | end 35 | 36 | local function get_collector_values(collector, result) 37 | for _, obs in ipairs(collector:collect()) do 38 | table.insert(result, obs) 39 | end 40 | end 41 | 42 | local function collect(opts) 43 | checks({invoke_callbacks = '?boolean', default_only = '?boolean'}) 44 | opts = opts or {} 45 | 46 | if opts.invoke_callbacks then 47 | registry:invoke_callbacks() 48 | end 49 | 50 | local result = {} 51 | for _, collector in pairs(registry.collectors) do 52 | if opts.default_only then 53 | if collector.metainfo.default then 54 | get_collector_values(collector, result) 55 | end 56 | else 57 | get_collector_values(collector, result) 58 | end 59 | end 60 | 61 | return result 62 | end 63 | 64 | local function clear() 65 | registry:clear() 66 | end 67 | 68 | local function counter(name, help, metainfo) 69 | checks('string', '?string', '?table') 70 | 71 | return registry:find_or_create(Counter, name, help, metainfo) 72 | end 73 | 74 | local function gauge(name, help, metainfo) 75 | checks('string', '?string', '?table') 76 | 77 | return registry:find_or_create(Gauge, name, help, metainfo) 78 | end 79 | 80 | local function histogram(name, help, buckets, metainfo) 81 | checks('string', '?string', '?table', '?table') 82 | if buckets ~= nil and not Histogram.check_buckets(buckets) then 83 | error('Invalid value for buckets') 84 | end 85 | 86 | return registry:find_or_create(Histogram, name, help, buckets, metainfo) 87 | end 88 | 89 | local function summary(name, help, objectives, params, metainfo) 90 | checks('string', '?string', '?table', { 91 | age_buckets_count = '?number', 92 | max_age_time = '?number', 93 | }, '?table') 94 | if objectives ~= nil and not Summary.check_quantiles(objectives) then 95 | error('Invalid value for objectives') 96 | end 97 | params = params or {} 98 | local age_buckets_count = params.age_buckets_count 99 | local max_age_time = params.max_age_time 100 | if max_age_time and max_age_time <= 0 then 101 | error('Max age must be positive') 102 | end 103 | if age_buckets_count and age_buckets_count < 1 then 104 | error('Age buckets count must be greater or equal than one') 105 | end 106 | if (max_age_time and not age_buckets_count) or (not max_age_time and age_buckets_count) then 107 | error('Age buckets count and max age must be present only together') 108 | end 109 | 110 | return registry:find_or_create(Summary, name, help, objectives, params, metainfo) 111 | end 112 | 113 | local function set_global_labels(label_pairs) 114 | checks('?table') 115 | 116 | label_pairs = label_pairs or {} 117 | 118 | -- Verify label table 119 | for k, _ in pairs(label_pairs) do 120 | if type(k) ~= 'string' then 121 | error(("bad label key (string expected, got %s)"):format(type(k))) 122 | end 123 | end 124 | 125 | registry:set_labels(label_pairs) 126 | end 127 | 128 | return { 129 | registry = registry, 130 | collectors = collectors, 131 | 132 | counter = counter, 133 | gauge = gauge, 134 | histogram = histogram, 135 | summary = summary, 136 | 137 | collect = collect, 138 | clear = clear, 139 | register_callback = register_callback, 140 | unregister_callback = unregister_callback, 141 | invoke_callbacks = invoke_callbacks, 142 | set_global_labels = set_global_labels, 143 | } 144 | -------------------------------------------------------------------------------- /metrics/cartridge/failover.lua: -------------------------------------------------------------------------------- 1 | local utils = require('metrics.utils') 2 | local collectors_list = {} 3 | local vars = nil 4 | 5 | local function update() 6 | if vars == nil then 7 | local is_cartridge = pcall(require, 'cartridge') 8 | if not is_cartridge then 9 | return 10 | end 11 | 12 | vars = require('cartridge.vars').new('cartridge.failover') 13 | end 14 | 15 | local trigger_cnt = vars.failover_trigger_cnt 16 | if trigger_cnt ~= nil then 17 | collectors_list.trigger_cnt = 18 | utils.set_counter( 19 | 'cartridge_failover_trigger_total', 20 | 'Count of Cartridge Failover triggers', 21 | trigger_cnt, 22 | nil, 23 | nil, 24 | {default = true} 25 | ) 26 | end 27 | end 28 | 29 | return { 30 | update = update, 31 | list = collectors_list, 32 | } 33 | -------------------------------------------------------------------------------- /metrics/cartridge/issues.lua: -------------------------------------------------------------------------------- 1 | local utils = require('metrics.utils') 2 | local fun = require('fun') 3 | 4 | local collectors_list = {} 5 | 6 | local function update() 7 | local list_on_instance = rawget(_G, '__cartridge_issues_list_on_instance') 8 | 9 | if not list_on_instance then 10 | return 11 | end 12 | 13 | local ok, issues = pcall(list_on_instance) 14 | 15 | if not ok then 16 | return 17 | end 18 | 19 | local levels = { 'warning', 'critical' } 20 | 21 | for _, level in ipairs(levels) do 22 | local len = fun.iter(issues):filter(function(x) return x.level == level end):length() 23 | collectors_list.cartridge_issues = 24 | utils.set_gauge('cartridge_issues', 'Tarantool Cartridge issues', 25 | len, {level = level}, nil, {default = true}) 26 | end 27 | 28 | local global_issues_cnt = rawget(_G, '__cartridge_issues_cnt') 29 | if global_issues_cnt ~= nil then 30 | collectors_list.global_issues = 31 | utils.set_gauge('cartridge_cluster_issues', 'Tarantool Cartridge cluster issues', 32 | global_issues_cnt, nil, nil, {default = true}) 33 | end 34 | end 35 | 36 | return { 37 | update = update, 38 | list = collectors_list, 39 | } 40 | -------------------------------------------------------------------------------- /metrics/cfg.lua: -------------------------------------------------------------------------------- 1 | -- Based on https://github.com/tarantool/crud/blob/73bf5bf9353f9b9ee69c95bb14c610be8f2daeac/crud/cfg.lua 2 | 3 | local checks = require('checks') 4 | 5 | local metrics_api = require('metrics.api') 6 | local const = require('metrics.const') 7 | local stash = require('metrics.stash') 8 | local metrics_tarantool = require('metrics.tarantool') 9 | 10 | local function set_defaults_if_empty(cfg) 11 | if cfg.include == nil then 12 | cfg.include = const.ALL 13 | end 14 | 15 | if cfg.exclude == nil then 16 | cfg.exclude = {} 17 | end 18 | 19 | if cfg.labels == nil then 20 | cfg.labels = {} 21 | end 22 | 23 | return cfg 24 | end 25 | 26 | local function configure(cfg, opts) 27 | if opts.include == nil then 28 | opts.include = cfg.include 29 | end 30 | 31 | if opts.exclude == nil then 32 | opts.exclude = cfg.exclude 33 | end 34 | 35 | if opts.labels == nil then 36 | opts.labels = cfg.labels 37 | end 38 | 39 | 40 | metrics_tarantool.enable_v2(opts.include, opts.exclude) 41 | metrics_api.set_global_labels(opts.labels) 42 | 43 | rawset(cfg, 'include', opts.include) 44 | rawset(cfg, 'exclude', opts.exclude) 45 | rawset(cfg, 'labels', opts.labels) 46 | end 47 | 48 | local _cfg = set_defaults_if_empty(stash.get(stash.name.cfg)) 49 | local _cfg_internal = stash.get(stash.name.cfg_internal) 50 | 51 | if _cfg_internal.initialized then 52 | configure(_cfg, {}) 53 | end 54 | 55 | local function __call(self, opts) 56 | checks('table', { 57 | include = '?string|table', 58 | exclude = '?table', 59 | labels = '?table', 60 | }) 61 | 62 | opts = table.deepcopy(opts) or {} 63 | 64 | configure(_cfg, opts) 65 | 66 | _cfg_internal.initialized = true 67 | 68 | return self 69 | end 70 | 71 | local function __index(_, key) 72 | if _cfg_internal.initialized then 73 | return _cfg[key] 74 | else 75 | error('Call metrics.cfg{} first') 76 | end 77 | end 78 | 79 | local function __newindex() 80 | error('Use metrics.cfg{} instead') 81 | end 82 | 83 | return { 84 | -- Iterating through `metrics.cfg` with pairs is not supported yet. 85 | cfg = setmetatable({}, { 86 | __index = __index, 87 | __newindex = __newindex, 88 | __call = __call, 89 | __serialize = function() return _cfg end 90 | }), 91 | } 92 | -------------------------------------------------------------------------------- /metrics/collectors/counter.lua: -------------------------------------------------------------------------------- 1 | local Shared = require('metrics.collectors.shared') 2 | 3 | local Counter = Shared:new_class('counter') 4 | 5 | function Counter:inc(num, label_pairs) 6 | if num ~= nil and type(tonumber(num)) ~= 'number' then 7 | error("Counter increment should be a number") 8 | end 9 | if num and num < 0 then 10 | error("Counter increment should not be negative") 11 | end 12 | Shared.inc(self, num, label_pairs) 13 | end 14 | 15 | function Counter:reset(label_pairs) 16 | Shared.set(self, 0, label_pairs) 17 | end 18 | 19 | return Counter 20 | -------------------------------------------------------------------------------- /metrics/collectors/gauge.lua: -------------------------------------------------------------------------------- 1 | local Shared = require('metrics.collectors.shared') 2 | 3 | local Gauge = Shared:new_class('gauge', {'inc', 'dec', 'set'}) 4 | 5 | return Gauge 6 | -------------------------------------------------------------------------------- /metrics/collectors/histogram.lua: -------------------------------------------------------------------------------- 1 | local log = require('log') 2 | 3 | local Shared = require('metrics.collectors.shared') 4 | local Counter = require('metrics.collectors.counter') 5 | 6 | local INF = math.huge 7 | local DEFAULT_BUCKETS = {.005, .01, .025, .05, .075, .1, .25, .5, 8 | .75, 1.0, 2.5, 5.0, 7.5, 10.0, INF} 9 | 10 | local Histogram = Shared:new_class('histogram', {'observe_latency'}) 11 | 12 | function Histogram.check_buckets(buckets) 13 | local prev = -math.huge 14 | for _, v in ipairs(buckets) do 15 | if type(v) ~= 'number' then return false end 16 | if v <= 0 then return false end 17 | if prev > v then return false end 18 | prev = v 19 | end 20 | return true 21 | end 22 | 23 | function Histogram:new(name, help, buckets, metainfo) 24 | metainfo = table.copy(metainfo) or {} 25 | local obj = Shared.new(self, name, help, metainfo) 26 | 27 | obj.buckets = buckets or DEFAULT_BUCKETS 28 | table.sort(obj.buckets) 29 | if obj.buckets[#obj.buckets] ~= INF then 30 | obj.buckets[#obj.buckets+1] = INF 31 | end 32 | 33 | obj.count_collector = Counter:new(name .. '_count', help, metainfo) 34 | obj.sum_collector = Counter:new(name .. '_sum', help, metainfo) 35 | obj.bucket_collector = Counter:new(name .. '_bucket', help, metainfo) 36 | 37 | return obj 38 | end 39 | 40 | function Histogram:set_registry(registry) 41 | Shared.set_registry(self, registry) 42 | self.count_collector:set_registry(registry) 43 | self.sum_collector:set_registry(registry) 44 | self.bucket_collector:set_registry(registry) 45 | end 46 | 47 | local cdata_warning_logged = false 48 | 49 | function Histogram:observe(num, label_pairs) 50 | label_pairs = label_pairs or {} 51 | if num ~= nil and type(tonumber(num)) ~= 'number' then 52 | error("Histogram observation should be a number") 53 | end 54 | if not cdata_warning_logged and type(num) == 'cdata' then 55 | log.error("Using cdata as observation in historgam " .. 56 | "can lead to unexpected results. " .. 57 | "That log message will be an error in the future.") 58 | cdata_warning_logged = true 59 | end 60 | 61 | self.count_collector:inc(1, label_pairs) 62 | self.sum_collector:inc(num, label_pairs) 63 | 64 | for _, bucket in ipairs(self.buckets) do 65 | local bkt_label_pairs = table.deepcopy(label_pairs) 66 | bkt_label_pairs.le = bucket 67 | 68 | if num <= bucket then 69 | self.bucket_collector:inc(1, bkt_label_pairs) 70 | else 71 | -- all buckets are needed for histogram quantile approximation 72 | -- this creates buckets if they were not created before 73 | self.bucket_collector:inc(0, bkt_label_pairs) 74 | end 75 | end 76 | end 77 | 78 | function Histogram:remove(label_pairs) 79 | assert(label_pairs, 'label pairs is a required parameter') 80 | self.count_collector:remove(label_pairs) 81 | self.sum_collector:remove(label_pairs) 82 | 83 | for _, bucket in ipairs(self.buckets) do 84 | local bkt_label_pairs = table.deepcopy(label_pairs) 85 | bkt_label_pairs.le = bucket 86 | self.bucket_collector:remove(bkt_label_pairs) 87 | end 88 | end 89 | 90 | function Histogram:collect() 91 | local result = {} 92 | for _, obs in ipairs(self.count_collector:collect()) do 93 | table.insert(result, obs) 94 | end 95 | for _, obs in ipairs(self.sum_collector:collect()) do 96 | table.insert(result, obs) 97 | end 98 | for _, obs in ipairs(self.bucket_collector:collect()) do 99 | table.insert(result, obs) 100 | end 101 | return result 102 | end 103 | 104 | return Histogram 105 | -------------------------------------------------------------------------------- /metrics/collectors/shared.lua: -------------------------------------------------------------------------------- 1 | local clock = require('clock') 2 | local fiber = require('fiber') 3 | local log = require('log') 4 | 5 | local Shared = {} 6 | 7 | -- Create collector class with the list of instance methods copied from 8 | -- this class (like an inheritance but with limited list of methods). 9 | function Shared:new_class(kind, method_names) 10 | method_names = method_names or {} 11 | -- essential methods 12 | table.insert(method_names, 'new') 13 | table.insert(method_names, 'set_registry') 14 | table.insert(method_names, 'make_key') 15 | table.insert(method_names, 'append_global_labels') 16 | table.insert(method_names, 'collect') 17 | table.insert(method_names, 'remove') 18 | local methods = {} 19 | for _, name in pairs(method_names) do 20 | methods[name] = self[name] 21 | end 22 | local class = {kind = kind} 23 | class.__index = class 24 | return setmetatable(class, {__index = methods}) 25 | end 26 | 27 | function Shared:new(name, help, metainfo) 28 | metainfo = table.copy(metainfo) or {} 29 | 30 | if not name then 31 | error("Name should be set for %s") 32 | end 33 | return setmetatable({ 34 | name = name, 35 | help = help or "", 36 | observations = {}, 37 | label_pairs = {}, 38 | metainfo = metainfo, 39 | }, self) 40 | end 41 | 42 | function Shared:set_registry(registry) 43 | self.registry = registry 44 | end 45 | 46 | function Shared.make_key(label_pairs) 47 | if type(label_pairs) ~= 'table' then 48 | return "" 49 | end 50 | local parts = {} 51 | for k, v in pairs(label_pairs) do 52 | table.insert(parts, k .. '\t' .. v) 53 | end 54 | table.sort(parts) 55 | return table.concat(parts, '\t') 56 | end 57 | 58 | function Shared:remove(label_pairs) 59 | assert(label_pairs, 'label pairs is a required parameter') 60 | local key = self.make_key(label_pairs) 61 | self.observations[key] = nil 62 | self.label_pairs[key] = nil 63 | end 64 | 65 | function Shared:set(num, label_pairs) 66 | if num ~= nil and type(tonumber(num)) ~= 'number' then 67 | error("Collector set value should be a number") 68 | end 69 | num = num or 0 70 | local key = self.make_key(label_pairs) 71 | self.observations[key] = num 72 | self.label_pairs[key] = label_pairs or {} 73 | end 74 | 75 | function Shared:inc(num, label_pairs) 76 | if num ~= nil and type(tonumber(num)) ~= 'number' then 77 | error("Collector increment should be a number") 78 | end 79 | num = num or 1 80 | local key = self.make_key(label_pairs) 81 | local old_value = self.observations[key] or 0 82 | self.observations[key] = old_value + num 83 | self.label_pairs[key] = label_pairs or {} 84 | end 85 | 86 | function Shared:dec(num, label_pairs) 87 | if num ~= nil and type(tonumber(num)) ~= 'number' then 88 | error("Collector decrement should be a number") 89 | end 90 | num = num or 1 91 | local key = self.make_key(label_pairs) 92 | local old_value = self.observations[key] or 0 93 | self.observations[key] = old_value - num 94 | self.label_pairs[key] = label_pairs or {} 95 | end 96 | 97 | local function log_observe_latency_error(err) 98 | log.error(debug.traceback('Saving metrics failed: ' .. tostring(err))) 99 | end 100 | 101 | local function observe_latency_tail(collector, label_pairs, start_time, ok, result, ...) 102 | local latency = clock.monotonic() - start_time 103 | if type(label_pairs) == 'function' then 104 | label_pairs = label_pairs(ok, result, ...) 105 | end 106 | xpcall( 107 | collector.observe, 108 | log_observe_latency_error, 109 | collector, latency, label_pairs 110 | ) 111 | if not ok then 112 | error(result) 113 | end 114 | return result, ... 115 | end 116 | 117 | --- Measure latency of function call 118 | -- 119 | -- @param label_pairs either table with labels or function to generate labels. 120 | -- If function is given its called with the results of pcall. 121 | -- @param fn function for pcall to instrument 122 | -- ... - args for function fn 123 | -- @return value from fn 124 | function Shared:observe_latency(label_pairs, fn, ...) 125 | return observe_latency_tail(self, label_pairs, clock.monotonic(), pcall(fn, ...)) 126 | end 127 | 128 | function Shared:append_global_labels(label_pairs) 129 | local global_labels = self.registry and self.registry.label_pairs 130 | if global_labels == nil or next(global_labels) == nil then 131 | return label_pairs 132 | end 133 | 134 | local extended_label_pairs = table.copy(label_pairs) 135 | 136 | for k, v in pairs(global_labels) do 137 | if extended_label_pairs[k] == nil then 138 | extended_label_pairs[k] = v 139 | end 140 | end 141 | 142 | return extended_label_pairs 143 | end 144 | 145 | function Shared:collect() 146 | if next(self.observations) == nil then 147 | return {} 148 | end 149 | local result = {} 150 | for key, observation in pairs(self.observations) do 151 | local obs = { 152 | metric_name = self.name, 153 | label_pairs = self:append_global_labels(self.label_pairs[key]), 154 | value = observation, 155 | timestamp = fiber.time64(), 156 | } 157 | table.insert(result, obs) 158 | end 159 | return result 160 | end 161 | 162 | return Shared 163 | -------------------------------------------------------------------------------- /metrics/collectors/summary.lua: -------------------------------------------------------------------------------- 1 | local Shared = require('metrics.collectors.shared') 2 | local Counter = require('metrics.collectors.counter') 3 | local Quantile = require('metrics.quantile') 4 | 5 | local fiber = require('fiber') 6 | 7 | local Summary = Shared:new_class('summary', {'observe_latency'}) 8 | 9 | function Summary:new(name, help, objectives, params, metainfo) 10 | params = params or {} 11 | metainfo = table.copy(metainfo) or {} 12 | local obj = Shared.new(self, name, help, metainfo) 13 | 14 | obj.count_collector = Counter:new(name .. '_count', help, metainfo) 15 | obj.sum_collector = Counter:new(name .. '_sum', help, metainfo) 16 | obj.objectives = objectives 17 | obj.max_age_time = params.max_age_time 18 | obj.age_buckets_count = params.age_buckets_count or 1 19 | obj.observations = {} 20 | 21 | if obj.objectives then 22 | obj.quantiles = {} 23 | for q, _ in pairs(objectives) do 24 | table.insert(obj.quantiles, q) 25 | end 26 | end 27 | return obj 28 | end 29 | 30 | function Summary.check_quantiles(objectives) 31 | for k, v in pairs(objectives) do 32 | if type(k) ~= 'number' then return false end 33 | if k > 1 or k < 0 then return false end 34 | if type(v) ~= 'number' then return false end 35 | end 36 | return true 37 | end 38 | 39 | function Summary:set_registry(registry) 40 | Shared.set_registry(self, registry) 41 | self.count_collector:set_registry(registry) 42 | self.sum_collector:set_registry(registry) 43 | end 44 | 45 | function Summary:rotate_age_buckets(key) 46 | local obs_object = self.observations[key] 47 | local old_index = obs_object.head_bucket_index 48 | obs_object.head_bucket_index = ((obs_object.head_bucket_index + 1) % self.age_buckets_count) + 1 49 | Quantile.Reset(obs_object.buckets[old_index]) 50 | obs_object.last_rotate = os.time() 51 | end 52 | 53 | function Summary:observe(num, label_pairs) 54 | label_pairs = label_pairs or {} 55 | if label_pairs.quantile then 56 | error('Label "quantile" are not allowed in summary') 57 | end 58 | if num ~= nil and type(tonumber(num)) ~= 'number' then 59 | error("Summary observation should be a number") 60 | end 61 | self.count_collector:inc(1, label_pairs) 62 | self.sum_collector:inc(num, label_pairs) 63 | if self.objectives then 64 | local now = os.time() 65 | local key = self.make_key(label_pairs) 66 | 67 | if not self.observations[key] then 68 | local obs_object = { 69 | buckets = {}, 70 | head_bucket_index = 1, 71 | last_rotate = now, 72 | label_pairs = label_pairs, 73 | } 74 | self.label_pairs[key] = label_pairs 75 | for i = 1, self.age_buckets_count do 76 | local quantile_obj = Quantile.NewTargeted(self.objectives) 77 | Quantile.Insert(quantile_obj, num) 78 | obs_object.buckets[i] = quantile_obj 79 | end 80 | self.observations[key] = obs_object 81 | else 82 | local obs_object = self.observations[key] 83 | if self.age_buckets_count > 1 and now - obs_object.last_rotate >= self.max_age_time then 84 | self:rotate_age_buckets(key) 85 | end 86 | for _, bucket in ipairs(obs_object.buckets) do 87 | Quantile.Insert(bucket, num) 88 | end 89 | end 90 | end 91 | end 92 | 93 | function Summary:remove(label_pairs) 94 | assert(label_pairs, 'label pairs is a required parameter') 95 | self.count_collector:remove(label_pairs) 96 | self.sum_collector:remove(label_pairs) 97 | if self.objectives then 98 | local key = self.make_key(label_pairs) 99 | self.observations[key] = nil 100 | end 101 | end 102 | 103 | function Summary:collect_quantiles() 104 | if not self.objectives or next(self.observations) == nil then 105 | return {} 106 | end 107 | 108 | local result = {} 109 | local now = os.time() 110 | for key, observation in pairs(self.observations) do 111 | if self.age_buckets_count > 1 and now - observation.last_rotate >= self.max_age_time then 112 | self:rotate_age_buckets(key) 113 | end 114 | for _, objective in ipairs(self.quantiles) do 115 | local label_pairs = table.deepcopy(self:append_global_labels(observation.label_pairs)) 116 | label_pairs.quantile = objective 117 | local obs = { 118 | metric_name = self.name, 119 | label_pairs = label_pairs, 120 | value = Quantile.Query(observation.buckets[observation.head_bucket_index], objective), 121 | timestamp = fiber.time64(), 122 | } 123 | table.insert(result, obs) 124 | end 125 | end 126 | return result 127 | end 128 | 129 | function Summary:collect() 130 | local result = {} 131 | for _, obs in ipairs(self.count_collector:collect()) do 132 | table.insert(result, obs) 133 | end 134 | for _, obs in ipairs(self.sum_collector:collect()) do 135 | table.insert(result, obs) 136 | end 137 | for _, obs in ipairs(self:collect_quantiles()) do 138 | table.insert(result, obs) 139 | end 140 | return result 141 | end 142 | 143 | -- debug function to get observation quantiles from summary 144 | -- returns array of quantile objects or 145 | -- single quantile object if summary has only one bucket 146 | function Summary:get_observations(label_pairs) 147 | local key = self.make_key(label_pairs or {}) 148 | local obs = self.observations[key] 149 | if self.age_buckets_count > 1 then 150 | return obs 151 | else 152 | return obs.buckets[1] 153 | end 154 | end 155 | 156 | return Summary 157 | -------------------------------------------------------------------------------- /metrics/const.lua: -------------------------------------------------------------------------------- 1 | return { 2 | INF = math.huge, 3 | NAN = math.huge * 0, 4 | 5 | ALL = 'all', 6 | NONE = 'none', 7 | } 8 | -------------------------------------------------------------------------------- /metrics/http_middleware.lua: -------------------------------------------------------------------------------- 1 | local export = {} 2 | local log = require('log') 3 | 4 | local metrics_api = require('metrics.api') 5 | 6 | local collector_type = { 7 | histogram = require('metrics.collectors.histogram'), 8 | summary = require('metrics.collectors.summary'), 9 | } 10 | 11 | export.DEFAULT_HISTOGRAM_BUCKETS = { 12 | 0.001, 0.0025, 0.005, 0.0075, 13 | 0.01, 0.025, 0.05, 0.075, 14 | 0.1, 0.25, 0.5, 0.75, 15 | 1.0, 2.5, 5.0, 7.5, 16 | 10.0, 17 | } 18 | 19 | export.DEFAULT_QUANTILES = { 20 | [0.5] = 0.01, 21 | [0.9] = 0.01, 22 | [0.99] = 0.01, 23 | } 24 | 25 | export.DEFAULT_SUMMARY_PARAMS = { 26 | max_age_time = 60, 27 | age_buckets_count = 5, 28 | } 29 | 30 | --- Build default histogram collector 31 | -- 32 | -- @string[opt='histogram'] type_name `histogram` or `summary` 33 | -- @string[opt='http_server_requests'] name 34 | -- @string[opt='HTTP Server Requests'] help 35 | -- @return collector 36 | function export.build_default_collector(type_name, name, help) 37 | type_name = type_name or 'histogram' 38 | name = name or 'http_server_request_latency' 39 | help = help or 'HTTP Server Request Latency' 40 | local extra = {} 41 | if type_name == 'histogram' then 42 | extra = {export.DEFAULT_HISTOGRAM_BUCKETS} 43 | elseif type_name == 'summary' then 44 | extra = {export.DEFAULT_QUANTILES, export.DEFAULT_SUMMARY_PARAMS} 45 | else 46 | error('Unknown collector type_name: ' .. tostring(type_name)) 47 | end 48 | return metrics_api.registry:register(collector_type[type_name]:new(name, help, unpack(extra))) 49 | end 50 | 51 | function export.get_default_collector() 52 | if not export.default_collector then 53 | export.default_collector = export.build_default_collector() 54 | end 55 | return export.default_collector 56 | end 57 | 58 | --- Set default collector for all middlewares 59 | -- 60 | -- @tab collector object with `:collect` method. 61 | function export.set_default_collector(collector) 62 | export.default_collector = collector 63 | end 64 | 65 | --- Build collector and set it as default 66 | -- 67 | -- @see build_default_collector 68 | function export.configure_default_collector(...) 69 | export.set_default_collector(export.build_default_collector(...)) 70 | end 71 | 72 | --- Measure latency and invoke collector with labels from given route 73 | -- 74 | -- @tab collector 75 | -- @tab route 76 | -- @string route.path 77 | -- @string route.method 78 | -- ... arguments for pcall to instrument 79 | -- @return value from observable function 80 | function export.observe(collector, route, handler, ...) 81 | return collector:observe_latency(function(ok, result) 82 | if ok ~= true then 83 | log.error('http handler returned %q', result) 84 | end 85 | if type(result) ~= 'table' then 86 | error(('incorrect http handler for %s %s: expecting return response object'): 87 | format(route.method, route.path), 0) 88 | end 89 | return { 90 | path = route.path, 91 | method = route.method, 92 | status = (not ok and 500) or result.status or 200, 93 | } 94 | end, handler, ...) 95 | end 96 | 97 | --- Apply instrumentation middleware for http request handler 98 | -- 99 | -- @func handler original 100 | -- @func[opt] collector custom histogram-like collector 101 | -- @return new handler 102 | -- @usage httpd:route({method = 'GET', path = '/...'}, http_middleware.v1(request_handler)) 103 | function export.v1(handler, collector) 104 | collector = collector or export.get_default_collector() 105 | return function(req) 106 | return export.observe(collector, req.endpoint, handler, req) 107 | end 108 | end 109 | 110 | return export 111 | -------------------------------------------------------------------------------- /metrics/init.lua: -------------------------------------------------------------------------------- 1 | -- vim: ts=4:sw=4:sts=4:expandtab 2 | 3 | local log = require('log') 4 | 5 | local api = require('metrics.api') 6 | local const = require('metrics.const') 7 | local cfg = require('metrics.cfg') 8 | local http_middleware = require('metrics.http_middleware') 9 | local tarantool = require('metrics.tarantool') 10 | 11 | local VERSION = require('metrics.version') 12 | 13 | return setmetatable({ 14 | registry = api.registry, 15 | 16 | counter = api.counter, 17 | gauge = api.gauge, 18 | histogram = api.histogram, 19 | summary = api.summary, 20 | 21 | INF = const.INF, 22 | NAN = const.NAN, 23 | 24 | clear = api.clear, 25 | collectors = api.collectors, 26 | register_callback = api.register_callback, 27 | unregister_callback = api.unregister_callback, 28 | invoke_callbacks = api.invoke_callbacks, 29 | set_global_labels = api.set_global_labels, 30 | enable_default_metrics = tarantool.enable, 31 | cfg = cfg.cfg, 32 | http_middleware = http_middleware, 33 | collect = api.collect, 34 | _VERSION = VERSION, 35 | }, { 36 | __index = function(_, key) 37 | if key == 'VERSION' then 38 | log.warn("require('metrics').VERSION is deprecated, " .. 39 | "use require('metrics')._VERSION instead.") 40 | return VERSION 41 | end 42 | 43 | return nil 44 | end 45 | }) 46 | -------------------------------------------------------------------------------- /metrics/plugins/graphite.lua: -------------------------------------------------------------------------------- 1 | local socket = require('socket') 2 | local fiber = require('fiber') 3 | local metrics = require('metrics') 4 | local checks = require('checks') 5 | local log = require('log') 6 | local fun = require('fun') 7 | 8 | local graphite = {} 9 | 10 | -- Default values 11 | local DEFAULT_PREFIX = 'tarantool' 12 | local DEFAULT_HOST = '127.0.0.1' 13 | local DEFAULT_PORT = 2003 14 | local DEFAULT_SEND_INTERVAL = 2 15 | 16 | -- Constants 17 | local LABELS_SEP = ';' 18 | 19 | function graphite.format_observation(prefix, obs) 20 | local metric_path = #prefix > 0 and ('%s.%s'):format(prefix, obs.metric_name) or obs.metric_name 21 | 22 | if next(obs.label_pairs) then 23 | local label_pairs_str_parts = {} 24 | for label, value in pairs(obs.label_pairs) do 25 | table.insert(label_pairs_str_parts, ('%s=%s'):format(label, value)) 26 | end 27 | local label_pairs_str = table.concat(label_pairs_str_parts, LABELS_SEP) 28 | metric_path = metric_path .. LABELS_SEP .. label_pairs_str 29 | end 30 | metric_path = metric_path:gsub(' ', '_') -- remove spaces (e.g. in values) 31 | local string_val = tostring(tonumber(obs.value)) -- removes ULL/LL suffixes 32 | 33 | local ts = tostring(obs.timestamp / 10^6):gsub("U*LL", "") -- Graphite takes time in seconds 34 | local graph = ('%s %s %s\n'):format(metric_path, string_val, ts) 35 | 36 | return graph 37 | end 38 | 39 | local function graphite_worker(opts) 40 | fiber.name('metrics_graphite_worker') 41 | 42 | while true do 43 | metrics.invoke_callbacks() 44 | for _, c in pairs(metrics.collectors()) do 45 | for _, obs in ipairs(c:collect()) do 46 | local data = graphite.format_observation(opts.prefix, obs) 47 | local numbytes = opts.sock:sendto(opts.host, opts.port, data) 48 | if numbytes == nil then 49 | log.error('Error while sending to host %s port %s data %s', 50 | opts.host, opts.port, data) 51 | end 52 | end 53 | end 54 | 55 | fiber.sleep(opts.send_interval) 56 | end 57 | end 58 | 59 | function graphite.init(opts) 60 | checks { 61 | prefix = '?string', 62 | host = '?string', 63 | port = '?number', 64 | send_interval = '?number' 65 | } 66 | 67 | local sock = socket('AF_INET', 'SOCK_DGRAM', 'udp') 68 | assert(sock ~= nil, 'Socket creation failed') 69 | 70 | local prefix = opts.prefix or DEFAULT_PREFIX 71 | local host = opts.host or DEFAULT_HOST 72 | local port = opts.port or DEFAULT_PORT 73 | local send_interval = opts.send_interval or DEFAULT_SEND_INTERVAL 74 | 75 | fun.iter(fiber.info()): 76 | filter(function(_, x) return x.name == 'metrics_graphite_worker' end): 77 | each(function(x) fiber.kill(x) end) 78 | 79 | fiber.create(graphite_worker, { 80 | prefix = prefix, 81 | sock = sock, 82 | host = host, 83 | port = port, 84 | send_interval = send_interval, 85 | }) 86 | end 87 | 88 | return graphite 89 | -------------------------------------------------------------------------------- /metrics/plugins/json.lua: -------------------------------------------------------------------------------- 1 | local metrics = require('metrics') 2 | local json = require('json') 3 | local json_exporter = {} 4 | 5 | local function finite(value) 6 | if type(value) == "string" then 7 | value = tonumber(value) 8 | if value == nil then return nil end 9 | elseif type(value) == "cdata" then -- support number64 10 | return value 11 | elseif type(value) ~= "number" then 12 | return nil 13 | end 14 | return value > -metrics.INF and value < metrics.INF 15 | end 16 | 17 | local function format_value(value) 18 | return finite(value) and value or tostring(value) 19 | end 20 | 21 | local function format_label_pairs(label_pairs) 22 | local part = {} 23 | if next(label_pairs) ~= nil then 24 | for name, value in pairs(label_pairs) do 25 | part[tostring(name)] = format_value(value) 26 | end 27 | end 28 | return part 29 | end 30 | 31 | local function format_observation(obs) 32 | local part = { 33 | metric_name = obs.metric_name, 34 | value = format_value(obs.value), 35 | label_pairs = format_label_pairs(obs.label_pairs), 36 | timestamp = obs.timestamp 37 | } 38 | return part 39 | end 40 | 41 | function json_exporter.export() 42 | metrics.invoke_callbacks() 43 | local stat = {} 44 | 45 | for _, c in pairs(metrics.collectors()) do 46 | for _, obs in ipairs(c:collect()) do 47 | local part = format_observation(obs) 48 | table.insert(stat, part) 49 | end 50 | end 51 | return json.encode(stat) 52 | end 53 | 54 | return json_exporter 55 | -------------------------------------------------------------------------------- /metrics/plugins/prometheus.lua: -------------------------------------------------------------------------------- 1 | local metrics = require('metrics') 2 | require('checks') 3 | 4 | local prometheus = {} 5 | 6 | local function escape(str) 7 | return str 8 | :gsub("\\", "\\\\") 9 | :gsub("\n", "\\n") 10 | :gsub('"', '\\"') 11 | end 12 | 13 | local function serialize_name(name) 14 | return escape(name) 15 | end 16 | 17 | local function serialize_value(value) 18 | local result 19 | if type(value) == 'cdata' then 20 | result = tostring(value) 21 | -- Luajit cdata type inserts some postfix in the end of the number after tostring() operation 22 | result = result:gsub("U*LL", "") 23 | elseif value == metrics.INF then 24 | result = '+Inf' 25 | elseif value == -metrics.INF then 26 | result = '-Inf' 27 | elseif value ~= value then 28 | result = 'Nan' 29 | else 30 | result = tostring(value) 31 | end 32 | return escape(result) 33 | end 34 | 35 | local function serialize_label_pairs(label_pairs) 36 | if next(label_pairs) == nil then 37 | return '' 38 | end 39 | 40 | local parts = {} 41 | for name, value in pairs(label_pairs) do 42 | local s = string.format('%s="%s"', 43 | serialize_value(name), serialize_value(value)) 44 | table.insert(parts, s) 45 | end 46 | 47 | local enumerated_via_comma = table.concat(parts, ',') 48 | return string.format('{%s}', enumerated_via_comma) 49 | end 50 | 51 | local function collect_and_serialize() 52 | metrics.invoke_callbacks() 53 | local parts = {} 54 | for _, c in pairs(metrics.collectors()) do 55 | table.insert(parts, string.format("# HELP %s %s", c.name, c.help)) 56 | table.insert(parts, string.format("# TYPE %s %s", c.name, c.kind)) 57 | for _, obs in ipairs(c:collect()) do 58 | local s = string.format('%s%s %s', 59 | serialize_name(obs.metric_name), 60 | serialize_label_pairs(obs.label_pairs), 61 | serialize_value(obs.value) 62 | ) 63 | table.insert(parts, s) 64 | end 65 | end 66 | return table.concat(parts, '\n') .. '\n' 67 | end 68 | 69 | function prometheus.collect_http() 70 | return { 71 | status = 200, 72 | headers = { ['content-type'] = 'text/plain; charset=utf8' }, 73 | body = collect_and_serialize(), 74 | } 75 | end 76 | 77 | return prometheus 78 | -------------------------------------------------------------------------------- /metrics/psutils/cpu.lua: -------------------------------------------------------------------------------- 1 | -- Linux is the only supported platform 2 | if jit.os ~= 'Linux' then 3 | return { update = function() end } 4 | end 5 | 6 | local utils = require('metrics.utils') 7 | local psutils = require('metrics.psutils.psutils_linux') 8 | 9 | local collectors_list = {} 10 | 11 | local instance_file = arg[0] 12 | 13 | local threads = {} 14 | 15 | local function update_cpu_metrics() 16 | collectors_list.cpu_number = utils.set_gauge('cpu_number', 'The number of processors', 17 | psutils.get_cpu_count(), nil, nil, {default = true}) 18 | 19 | collectors_list.cpu_time = utils.set_gauge('cpu_time', 'Host CPU time', 20 | psutils.get_cpu_time(), nil, nil, {default = true}) 21 | 22 | local new_threads = {} 23 | for _, thread_info in ipairs(psutils.get_process_cpu_time()) do 24 | local labels = { 25 | thread_name = thread_info.comm, 26 | thread_pid = thread_info.pid, 27 | file_name = instance_file, 28 | } 29 | 30 | local utime_labels = table.copy(labels) 31 | utime_labels.kind = 'user' 32 | collectors_list.cpu_thread = utils.set_gauge('cpu_thread', 'Tarantool thread cpu time', 33 | thread_info.utime, utime_labels, nil, {default = true}) 34 | 35 | local stime_labels = table.copy(labels) 36 | stime_labels.kind = 'system' 37 | collectors_list.cpu_thread = utils.set_gauge('cpu_thread', 'Tarantool thread cpu time', 38 | thread_info.stime, stime_labels, nil, {default = true}) 39 | 40 | threads[thread_info.pid] = nil 41 | new_threads[thread_info.pid] = labels 42 | end 43 | 44 | for _, thread_info in pairs(threads) do 45 | thread_info.kind = 'user' 46 | collectors_list.cpu_thread:remove(thread_info) 47 | 48 | thread_info.kind = 'system' 49 | collectors_list.cpu_thread:remove(thread_info) 50 | end 51 | threads = new_threads 52 | end 53 | 54 | local function clear_cpu_metrics() 55 | utils.delete_collectors(collectors_list) 56 | end 57 | 58 | return { 59 | update = update_cpu_metrics, 60 | list = collectors_list, 61 | clear = clear_cpu_metrics, 62 | } 63 | -------------------------------------------------------------------------------- /metrics/psutils/psutils_linux.lua: -------------------------------------------------------------------------------- 1 | local fio = require('fio') 2 | local string = require('string') 3 | local ffi = require('ffi') 4 | 5 | local get_nprocs_conf = function() end 6 | if jit.os == 'Linux' then 7 | ffi.cdef[[ 8 | int get_nprocs_conf(void); 9 | ]] 10 | get_nprocs_conf = ffi.C.get_nprocs_conf 11 | end 12 | 13 | local function get_cpu_time() 14 | local stat_file_path = '/proc/stat' 15 | local cpu_stat_file = fio.open(stat_file_path, 'O_RDONLY') 16 | if cpu_stat_file == nil then 17 | return nil 18 | end 19 | 20 | local stats_raw = cpu_stat_file:read(512) 21 | cpu_stat_file:close() 22 | if #stats_raw == 0 then 23 | return nil 24 | end 25 | 26 | local stats = string.split(stats_raw, '\n') 27 | local cpu_times = string.split(stats[1]) 28 | 29 | local sum = 0 30 | for i, cpu_time in ipairs(cpu_times) do 31 | if i > 1 then 32 | sum = sum + tonumber(cpu_time) 33 | end 34 | end 35 | 36 | return sum 37 | end 38 | 39 | local function parse_process_stat(path) 40 | local stat = fio.open(path, 'O_RDONLY') 41 | if stat == nil then 42 | print('stat open error') 43 | return nil 44 | end 45 | 46 | local s = stat:read(512) 47 | stat:close() 48 | 49 | local stats = string.split(s) 50 | return { 51 | pid = tonumber(stats[1]), 52 | comm = stats[2]:gsub('[()]', ''), -- strip spaces 53 | utime = tonumber(stats[14]), 54 | stime = tonumber(stats[15]), 55 | } 56 | end 57 | 58 | local function get_process_cpu_time() 59 | local task_path = '/proc/self/task' 60 | local threads = fio.listdir(task_path) 61 | local thread_time = {} 62 | for i, thread_pid in ipairs(threads) do 63 | thread_time[i] = parse_process_stat(task_path .. '/' .. thread_pid .. '/stat') 64 | end 65 | 66 | return thread_time 67 | end 68 | 69 | return { 70 | get_cpu_time = get_cpu_time, 71 | get_process_cpu_time = get_process_cpu_time, 72 | get_cpu_count = get_nprocs_conf, 73 | } 74 | -------------------------------------------------------------------------------- /metrics/registry.lua: -------------------------------------------------------------------------------- 1 | local Registry = {} 2 | Registry.__index = Registry 3 | 4 | function Registry.new() 5 | local obj = {} 6 | setmetatable(obj, Registry) 7 | obj:clear() 8 | return obj 9 | end 10 | 11 | function Registry:clear() 12 | self.collectors = {} 13 | self.callbacks = {} 14 | self.label_pairs = {} 15 | end 16 | 17 | function Registry:find(kind, name) 18 | return self.collectors[name .. kind] 19 | end 20 | 21 | function Registry:find_or_create(class, name, ...) 22 | return self:find(class.kind, name) or self:register(class:new(name, ...)) 23 | end 24 | 25 | local function is_empty(str) 26 | return str == nil or str == '' 27 | end 28 | 29 | function Registry:register(collector) 30 | assert(collector ~= nil, 'Collector is empty') 31 | assert(not is_empty(collector.name), "Collector's name is empty") 32 | assert(not is_empty(collector.kind), "Collector's kind is empty") 33 | if self:find(collector.kind, collector.name) then 34 | error('Already registered') 35 | end 36 | collector:set_registry(self) 37 | self.collectors[collector.name .. collector.kind] = collector 38 | return collector 39 | end 40 | 41 | function Registry:unregister(collector) 42 | self.collectors[collector.name .. collector.kind] = nil 43 | end 44 | 45 | function Registry:invoke_callbacks() 46 | for registered_callback, _ in pairs(self.callbacks) do 47 | registered_callback() 48 | end 49 | end 50 | 51 | function Registry:collect() 52 | local result = {} 53 | for _, collector in pairs(self.collectors) do 54 | for _, obs in ipairs(collector:collect()) do 55 | table.insert(result, obs) 56 | end 57 | end 58 | return result 59 | end 60 | 61 | function Registry:register_callback(callback) 62 | self.callbacks[callback] = true 63 | end 64 | 65 | function Registry:unregister_callback(callback) 66 | self.callbacks[callback] = nil 67 | end 68 | 69 | function Registry:set_labels(label_pairs) 70 | self.label_pairs = table.copy(label_pairs) 71 | end 72 | 73 | return Registry 74 | -------------------------------------------------------------------------------- /metrics/stash.lua: -------------------------------------------------------------------------------- 1 | -- Based on https://github.com/tarantool/crud/blob/73bf5bf9353f9b9ee69c95bb14c610be8f2daeac/crud/common/stash.lua 2 | 3 | local stash = {} 4 | 5 | --- Available stashes list. 6 | -- 7 | -- @tfield string cfg 8 | -- Stash for metrics module configuration. 9 | -- 10 | stash.name = { 11 | cfg = '__metrics_cfg', 12 | cfg_internal = '__metrics_cfg_internal' 13 | } 14 | 15 | --- Setup Tarantool Cartridge reload. 16 | -- 17 | -- @function setup_cartridge_reload 18 | -- 19 | -- @return Returns 20 | -- 21 | function stash.setup_cartridge_reload() 22 | local hotreload = require('cartridge.hotreload') 23 | for _, name in pairs(stash.name) do 24 | hotreload.whitelist_globals({ name }) 25 | end 26 | end 27 | 28 | --- Get a stash instance, initialize if needed. 29 | -- 30 | -- Stashes are persistent to package reload. 31 | -- To use them with Cartridge roles reload, 32 | -- call `stash.setup_cartridge_reload` in role. 33 | -- 34 | -- @function get 35 | -- 36 | -- @string name 37 | -- Stash identifier. Use one from `stash.name` table. 38 | -- 39 | -- @treturn table A stash instance. 40 | -- 41 | function stash.get(name) 42 | local instance = rawget(_G, name) or {} 43 | rawset(_G, name, instance) 44 | 45 | return instance 46 | end 47 | 48 | return stash 49 | -------------------------------------------------------------------------------- /metrics/tarantool.lua: -------------------------------------------------------------------------------- 1 | local log = require("log") 2 | 3 | local metrics_api = require('metrics.api') 4 | local utils = require('metrics.utils') 5 | local const = require('metrics.const') 6 | 7 | local default_metrics = { 8 | -- category = {update: function, list: table}, 9 | network = require('metrics.tarantool.network'), 10 | operations = require('metrics.tarantool.operations'), 11 | system = require('metrics.tarantool.system'), 12 | replicas = require('metrics.tarantool.replicas'), 13 | info = require('metrics.tarantool.info'), 14 | slab = require('metrics.tarantool.slab'), 15 | runtime = require('metrics.tarantool.runtime'), 16 | memory = require('metrics.tarantool.memory'), 17 | spaces = require('metrics.tarantool.spaces'), 18 | fibers = require('metrics.tarantool.fibers'), 19 | cpu = require('metrics.tarantool.cpu'), 20 | vinyl = require('metrics.tarantool.vinyl'), 21 | memtx = require('metrics.tarantool.memtx'), 22 | luajit = require('metrics.tarantool.luajit'), 23 | cartridge_issues = require('metrics.cartridge.issues'), 24 | cartridge_failover = require('metrics.cartridge.failover'), 25 | clock = require('metrics.tarantool.clock'), 26 | event_loop = require('metrics.tarantool.event_loop'), 27 | config = require('metrics.tarantool.config'), 28 | cpu_extended = require('metrics.psutils.cpu'), 29 | } 30 | 31 | local all_metrics_map = {} 32 | for name, _ in pairs(default_metrics) do 33 | all_metrics_map[name] = true 34 | end 35 | 36 | local function check_metrics_name(name, raise_if_unknown) 37 | if default_metrics[name] == nil then 38 | if raise_if_unknown then 39 | error(string.format("Unknown metrics %q provided", name)) 40 | else 41 | log.warn("Unknown metrics %q provided, this will raise an error in the future", name) 42 | end 43 | end 44 | end 45 | 46 | local function enable_impl(include, exclude, raise_if_unknown) 47 | include = include or const.ALL 48 | exclude = exclude or {} 49 | 50 | local include_map = {} 51 | 52 | if include == const.ALL then 53 | include_map = table.deepcopy(all_metrics_map) 54 | elseif type(include) == 'table' then 55 | for _, name in pairs(include) do 56 | if name == const.ALL then -- metasection "all" 57 | include_map = table.deepcopy(all_metrics_map) 58 | else 59 | check_metrics_name(name, raise_if_unknown) 60 | include_map[name] = true 61 | end 62 | end 63 | elseif include == const.NONE then 64 | include_map = {} 65 | else 66 | error('Unexpected value provided: include must be "all", {...} or "none"') 67 | end 68 | 69 | for _, name in pairs(exclude) do 70 | if name == const.ALL then -- metasection "all" 71 | include_map = {} 72 | else 73 | check_metrics_name(name, raise_if_unknown) 74 | include_map[name] = false 75 | end 76 | end 77 | 78 | for name, value in pairs(default_metrics) do 79 | if include_map[name] then 80 | metrics_api.register_callback(value.update) 81 | else 82 | metrics_api.unregister_callback(value.update) 83 | utils.delete_collectors(value.list) 84 | end 85 | end 86 | end 87 | 88 | local function is_empty_table(v) 89 | return (type(v) == 'table') and (next(v) == nil) 90 | end 91 | 92 | local function enable(include, exclude) 93 | -- Compatibility with v1. 94 | if is_empty_table(include) then 95 | log.warn('Providing {} in enable_default_metrics include is treated ' .. 96 | 'as a default value now (i.e. include all), ' .. 97 | 'but it will change in the future. Use "all" instead') 98 | include = const.ALL 99 | end 100 | 101 | return enable_impl(include, exclude, false) 102 | end 103 | 104 | local function enable_v2(include, exclude) 105 | return enable_impl(include, exclude, true) 106 | end 107 | 108 | return { 109 | enable = enable, 110 | enable_v2 = enable_v2, 111 | } 112 | -------------------------------------------------------------------------------- /metrics/tarantool/clock.lua: -------------------------------------------------------------------------------- 1 | local utils = require('metrics.utils') 2 | 3 | local collectors_list = {} 4 | 5 | -- from https://github.com/tarantool/cartridge/blob/cc607f5a6508449608f3953a3f93669e8c8c4ab0/cartridge/issues.lua#L375 6 | local function update_clock_metrics() 7 | local ok, membership = pcall(require, 'membership') 8 | if not ok then 9 | return 10 | end 11 | 12 | local min_delta = 0 13 | local max_delta = 0 14 | 15 | for _, member in membership.pairs() do 16 | if member and member.status == 'alive' and member.clock_delta ~= nil then 17 | if member.clock_delta < min_delta then 18 | min_delta = member.clock_delta 19 | end 20 | 21 | if member.clock_delta > max_delta then 22 | max_delta = member.clock_delta 23 | end 24 | end 25 | end 26 | 27 | collectors_list.clock_delta = utils.set_gauge('clock_delta', 'Clock difference', 28 | min_delta * 1e-6, {delta = 'min'}, nil, {default = true}) 29 | collectors_list.clock_delta = utils.set_gauge('clock_delta', 'Clock difference', 30 | max_delta * 1e-6, {delta = 'max'}, nil, {default = true}) 31 | end 32 | 33 | return { 34 | update = update_clock_metrics, 35 | list = collectors_list, 36 | } 37 | -------------------------------------------------------------------------------- /metrics/tarantool/config.lua: -------------------------------------------------------------------------------- 1 | local utils = require('metrics.utils') 2 | 3 | local collectors_list = {} 4 | 5 | local function get_config_alerts(config_info) 6 | -- https://github.com/tarantool/tarantool/blob/319357d5973d15d08b8eda6a230eada08b710802/src/box/lua/config/utils/aboard.lua#L17-L18 7 | local config_alerts = { 8 | warn = 0, 9 | error = 0, 10 | } 11 | 12 | for _, alert in pairs(config_info.alerts) do 13 | config_alerts[alert.type] = config_alerts[alert.type] + 1 14 | end 15 | 16 | return config_alerts 17 | end 18 | 19 | local function get_config_status(config_info) 20 | -- See state diagram here 21 | -- https://github.com/tarantool/doc/issues/3544#issuecomment-1866033480 22 | local config_status = { 23 | uninitialized = 0, 24 | startup_in_progress = 0, 25 | reload_in_progress = 0, 26 | check_warnings = 0, 27 | check_errors = 0, 28 | ready = 0, 29 | } 30 | 31 | config_status[config_info.status] = 1 32 | 33 | return config_status 34 | end 35 | 36 | local function update() 37 | if not utils.is_tarantool3() then 38 | return 39 | end 40 | 41 | -- Can migrate to box.info().config later 42 | -- https://github.com/tarantool/tarantool/commit/a1544d3bbc029c6fb2a148e580afe2b20e269b8d 43 | local config = require('config') 44 | local config_info = config:info() 45 | 46 | local config_alerts = get_config_alerts(config_info) 47 | 48 | for level, count in pairs(config_alerts) do 49 | collectors_list.config_alerts = utils.set_gauge( 50 | 'config_alerts', 51 | 'Tarantool 3 configuration alerts', 52 | count, 53 | {level = level}, 54 | nil, 55 | {default = true} 56 | ) 57 | end 58 | 59 | local config_status = get_config_status(config_info) 60 | 61 | for status, value in pairs(config_status) do 62 | collectors_list.config_status = utils.set_gauge( 63 | 'config_status', 64 | 'Tarantool 3 configuration status', 65 | value, 66 | {status = status}, 67 | nil, 68 | {default = true} 69 | ) 70 | end 71 | end 72 | 73 | return { 74 | update = update, 75 | list = collectors_list, 76 | } 77 | -------------------------------------------------------------------------------- /metrics/tarantool/cpu.lua: -------------------------------------------------------------------------------- 1 | local ffi = require('ffi') 2 | local utils = require('metrics.utils') 3 | 4 | local collectors_list = {} 5 | 6 | if not pcall(ffi.typeof, "struct timeval") then 7 | if ffi.os == 'OSX' then 8 | ffi.cdef[[ 9 | typedef int32_t suseconds_t; 10 | struct timeval { 11 | long tv_sec; /* seconds */ 12 | suseconds_t tv_usec; /* microseconds */ 13 | }; 14 | ]] 15 | else 16 | ffi.cdef[[ 17 | struct timeval { 18 | long tv_sec; /* seconds */ 19 | long tv_usec; /* microseconds */ 20 | }; 21 | ]] 22 | end 23 | end 24 | 25 | if not pcall(ffi.typeof, "struct rusage") then 26 | ffi.cdef[[ 27 | struct rusage { 28 | struct timeval ru_utime; /* user CPU time used */ 29 | struct timeval ru_stime; /* system CPU time used */ 30 | long ru_maxrss; /* maximum resident set size */ 31 | long ru_ixrss; /* integral shared memory size */ 32 | long ru_idrss; /* integral unshared data size */ 33 | long ru_isrss; /* integral unshared stack size */ 34 | long ru_minflt; /* page reclaims (soft page faults) */ 35 | long ru_majflt; /* page faults (hard page faults) */ 36 | long ru_nswap; /* swaps */ 37 | long ru_inblock; /* block input operations */ 38 | long ru_oublock; /* block output operations */ 39 | long ru_msgsnd; /* IPC messages sent */ 40 | long ru_msgrcv; /* IPC messages received */ 41 | long ru_nsignals; /* signals received */ 42 | long ru_nvcsw; /* voluntary context switches */ 43 | long ru_nivcsw; /* involuntary context switches */ 44 | }; 45 | int getrusage(int who, struct rusage *usage); 46 | int gettimeofday(struct timeval *tv, struct timezone *tz); 47 | ]] 48 | end 49 | 50 | local RUSAGE_SELF = 0 51 | 52 | local shared_rusage = ffi.new("struct rusage[1]") 53 | 54 | local function ss_get_rusage() 55 | if ffi.C.getrusage(RUSAGE_SELF, shared_rusage) < 0 then 56 | return nil 57 | end 58 | 59 | local ru_utime = tonumber(shared_rusage[0].ru_utime.tv_sec) + 60 | (tonumber(shared_rusage[0].ru_utime.tv_usec) / 1000000) 61 | local ru_stime = tonumber(shared_rusage[0].ru_stime.tv_sec) + 62 | (tonumber(shared_rusage[0].ru_stime.tv_usec) / 1000000) 63 | 64 | return { 65 | ru_utime = ru_utime, 66 | ru_stime = ru_stime, 67 | } 68 | end 69 | 70 | local function update_info_metrics() 71 | local cpu_time = ss_get_rusage() 72 | if cpu_time then 73 | collectors_list.cpu_user_time = utils.set_gauge('cpu_user_time', 'CPU user time usage', 74 | cpu_time.ru_utime, nil, nil, {default = true}) 75 | collectors_list.cpu_system_time = utils.set_gauge('cpu_system_time', 'CPU system time usage', 76 | cpu_time.ru_stime, nil, nil, {default = true}) 77 | end 78 | end 79 | 80 | return { 81 | update = update_info_metrics, 82 | list = collectors_list, 83 | } 84 | -------------------------------------------------------------------------------- /metrics/tarantool/event_loop.lua: -------------------------------------------------------------------------------- 1 | local utils = require('metrics.utils') 2 | local fiber = require('fiber') 3 | local clock = require('clock') 4 | 5 | local collectors_list = {} 6 | 7 | local ev_now_64 = function() return 1e3*fiber.time64() end 8 | 9 | local function evloop_time() 10 | fiber.sleep(0) -- setups watcher to the start of next ev_run 11 | 12 | local e0, w0 = ev_now_64(), clock.time64() 13 | fiber.sleep(0) -- rotates single loop 14 | local e1, w1 = ev_now_64(), clock.time64() 15 | 16 | -- e0 <= w0 <= e1 <= w1 (if ntp is ok) 17 | local ev_loop_time, ev_prolog, ev_epilog 18 | if e1 <= w1 then 19 | -- lag from the start of the loop till current fiber (must be as little as possible) 20 | ev_prolog = w1-e1 21 | else 22 | ev_prolog = 0 23 | end 24 | if w0 <= e1 then 25 | -- the epilog of previous ev_once 26 | ev_epilog = e1-w0 27 | else 28 | ev_epilog = 0 29 | end 30 | 31 | if e0 < e1 then 32 | -- diff between 2 neighbour ev_once's 33 | ev_loop_time = e1-e0 34 | else 35 | ev_loop_time = 0 36 | end 37 | 38 | -- convert to double to get seconds precision 39 | return tonumber(ev_loop_time/1e3)/1e3, tonumber(ev_prolog/1e3)/1e3, tonumber(ev_epilog/1e3)/1e3 40 | end 41 | 42 | 43 | local function update_info_metrics() 44 | local ev_once_time, ev_prolog, ev_epilog = evloop_time() 45 | collectors_list.ev_loop_time = utils.set_gauge('ev_loop_time', 'Event loop time (ms)', 46 | ev_once_time, nil, nil, {default = true}) 47 | collectors_list.ev_prolog_time = utils.set_gauge('ev_loop_prolog_time', 'Event loop prolog time (ms)', 48 | ev_prolog, nil, nil, {default = true}) 49 | collectors_list.ev_epilog_time = utils.set_gauge('ev_loop_epilog_time', 'Event loop epilog time (ms)', 50 | ev_epilog, nil, nil, {default = true}) 51 | end 52 | 53 | return { 54 | update = update_info_metrics, 55 | list = collectors_list, 56 | } 57 | -------------------------------------------------------------------------------- /metrics/tarantool/fibers.lua: -------------------------------------------------------------------------------- 1 | local fiber = require('fiber') 2 | local utils = require('metrics.utils') 3 | 4 | local collectors_list = {} 5 | 6 | local function update_fibers_metrics() 7 | local fibers_info = fiber.info({backtrace = false}) 8 | local fibers = 0 9 | local csws = 0 10 | local falloc = 0 11 | local fused = 0 12 | 13 | for _, f in pairs(fibers_info) do 14 | fibers = fibers + 1 15 | csws = csws + f.csw 16 | falloc = falloc + f.memory.total 17 | fused = fused + f.memory.used 18 | end 19 | 20 | collectors_list.fiber_amount = utils.set_gauge('fiber_amount', 'Amount of fibers', 21 | fibers, nil, nil, {default = true}) 22 | collectors_list.fiber_csw = utils.set_gauge('fiber_csw', 'Fibers csw', 23 | csws, nil, nil, {default = true}) 24 | collectors_list.fiber_memalloc = utils.set_gauge('fiber_memalloc', 'Fibers memalloc', 25 | falloc, nil, nil, {default = true}) 26 | collectors_list.fiber_memused = utils.set_gauge('fiber_memused', 'Fibers memused', 27 | fused, nil, nil, {default = true}) 28 | end 29 | 30 | return { 31 | update = update_fibers_metrics, 32 | list = collectors_list, 33 | } 34 | -------------------------------------------------------------------------------- /metrics/tarantool/info.lua: -------------------------------------------------------------------------------- 1 | local utils = require('metrics.utils') 2 | 3 | local collectors_list = {} 4 | 5 | local read_only_status = { 6 | [true] = 1, 7 | [false] = 0, 8 | } 9 | 10 | local election_states = { 11 | follower = 0, 12 | candidate = 1, 13 | leader = 2, 14 | } 15 | 16 | local function update_info_metrics() 17 | if not utils.box_is_configured() then 18 | return 19 | end 20 | 21 | local info = box.info() 22 | 23 | collectors_list.info_lsn = utils.set_gauge('info_lsn', 'Tarantool lsn', 24 | info.lsn, nil, nil, {default = true}) 25 | collectors_list.info_uptime = utils.set_gauge('info_uptime', 'Tarantool uptime', 26 | info.uptime, nil, nil, {default = true}) 27 | 28 | for k, v in pairs(info.vclock) do 29 | collectors_list.info_vclock = utils.set_gauge('info_vclock', 'VClock', 30 | v, {id = k}, nil, {default = true}) 31 | end 32 | 33 | for k, v in pairs(info.replication) do 34 | if k ~= info.id then 35 | if v.upstream ~= nil then 36 | collectors_list.replication_lag = 37 | utils.set_gauge('replication_lag', 'Replication lag', 38 | v.upstream.lag, {stream = 'upstream', id = k}, nil, {default = true}) 39 | collectors_list.replication_status = 40 | utils.set_gauge('replication_status', 'Replication status', 41 | v.upstream.status == 'follow' and 1 or 0, {stream = 'upstream', id = k}, 42 | nil, {default = true}) 43 | end 44 | if v.downstream ~= nil then 45 | collectors_list.replication_lag = 46 | utils.set_gauge('replication_lag', 'Replication lag', 47 | v.downstream.lag, {stream = 'downstream', id = k}, 48 | nil, {default = true}) 49 | collectors_list.replication_status = 50 | utils.set_gauge('replication_status', 'Replication status', 51 | v.downstream.status == 'follow' and 1 or 0, {stream = 'downstream', id = k}, 52 | nil, {default = true}) 53 | end 54 | end 55 | end 56 | 57 | collectors_list.read_only = utils.set_gauge('read_only', 'Is instance read only', 58 | read_only_status[info.ro], nil, nil, {default = true}) 59 | 60 | if info.synchro ~= nil then 61 | collectors_list.synchro_queue_owner = 62 | utils.set_gauge('synchro_queue_owner', 'Synchro queue owner', 63 | info.synchro.queue.owner, nil, nil, {default = true}) 64 | 65 | collectors_list.synchro_queue_term = 66 | utils.set_gauge('synchro_queue_term', 'Synchro queue term', 67 | info.synchro.queue.term, nil, nil, {default = true}) 68 | 69 | collectors_list.synchro_queue_len = 70 | utils.set_gauge('synchro_queue_len', 'Amount of transactions are collecting confirmations now', 71 | info.synchro.queue.len, nil, nil, {default = true}) 72 | 73 | collectors_list.synchro_queue_busy = 74 | utils.set_gauge('synchro_queue_busy', 'Is synchro queue busy', 75 | info.synchro.busy == true and 1 or 0, nil, nil, {default = true}) 76 | end 77 | 78 | if info.election ~= nil then 79 | collectors_list.election_state = 80 | utils.set_gauge('election_state', 'Election state of the node', 81 | election_states[info.election.state], nil, nil, {default = true}) 82 | 83 | collectors_list.election_vote = 84 | utils.set_gauge('election_vote', 'ID of a node the current node votes for', 85 | info.election.vote, nil, nil, {default = true}) 86 | 87 | collectors_list.election_leader = 88 | utils.set_gauge('election_leader', 'Leader node ID in the current term', 89 | info.election.leader, nil, nil, {default = true}) 90 | 91 | collectors_list.election_term = 92 | utils.set_gauge('election_term', 'Current election term', 93 | info.election.term, nil, nil, {default = true}) 94 | 95 | if info.election.leader_idle ~= nil then 96 | collectors_list.election_leader_idle = 97 | utils.set_gauge('election_leader_idle', 'Current idle for elected leader', 98 | info.election.leader_idle, nil, nil, {default = true}) 99 | end 100 | end 101 | end 102 | 103 | return { 104 | update = update_info_metrics, 105 | list = collectors_list, 106 | } 107 | -------------------------------------------------------------------------------- /metrics/tarantool/luajit.lua: -------------------------------------------------------------------------------- 1 | local has_mics_module, misc = pcall(require, 'misc') 2 | 3 | local LJ_PREFIX = 'lj_' 4 | 5 | local utils = require('metrics.utils') 6 | 7 | local collectors_list = {} 8 | 9 | local function update() 10 | if not (has_mics_module and misc.getmetrics ~= nil) then 11 | return 12 | end 13 | -- Details: https://github.com/tarantool/doc/issues/1597 14 | local lj_metrics = misc.getmetrics() 15 | collectors_list.gc_freed_total = 16 | utils.set_counter('gc_freed_total', 'Total amount of freed memory', 17 | lj_metrics.gc_freed, nil, LJ_PREFIX, {default = true}) 18 | collectors_list.strhash_hit_total = 19 | utils.set_counter('strhash_hit_total', 'Total number of strings being interned', 20 | lj_metrics.strhash_hit, nil, LJ_PREFIX, {default = true}) 21 | collectors_list.gc_steps_atomic_total = 22 | utils.set_counter('gc_steps_atomic_total', 'Total count of incremental GC steps (atomic state)', 23 | lj_metrics.gc_steps_atomic, nil, LJ_PREFIX, {default = true}) 24 | collectors_list.strhash_miss_total = 25 | utils.set_counter('strhash_miss_total', 'Total number of strings allocations during the platform lifetime', 26 | lj_metrics.strhash_miss, nil, LJ_PREFIX, {default = true}) 27 | collectors_list.gc_steps_sweepstring_total = 28 | utils.set_counter('gc_steps_sweepstring_total', 'Total count of incremental GC steps (sweepstring state)', 29 | lj_metrics.gc_steps_sweepstring, nil, LJ_PREFIX, {default = true}) 30 | collectors_list.gc_strnum = 31 | utils.set_gauge('gc_strnum', 'Amount of allocated string objects', 32 | lj_metrics.gc_strnum, nil, LJ_PREFIX, {default = true}) 33 | collectors_list.gc_tabnum = 34 | utils.set_gauge('gc_tabnum', 'Amount of allocated table objects', 35 | lj_metrics.gc_tabnum, nil, LJ_PREFIX, {default = true}) 36 | collectors_list.gc_cdatanum = 37 | utils.set_gauge('gc_cdatanum', 'Amount of allocated cdata objects', 38 | lj_metrics.gc_cdatanum, nil, LJ_PREFIX, {default = true}) 39 | collectors_list.jit_snap_restore_total = 40 | utils.set_counter('jit_snap_restore_total', 'Overall number of snap restores', 41 | lj_metrics.jit_snap_restore, nil, LJ_PREFIX, {default = true}) 42 | collectors_list.gc_memory = 43 | utils.set_gauge('gc_memory', 'Memory currently allocated', 44 | lj_metrics.gc_total, nil, LJ_PREFIX, {default = true}) 45 | collectors_list.gc_udatanum = 46 | utils.set_gauge('gc_udatanum', 'Amount of allocated udata objects', 47 | lj_metrics.gc_udatanum, nil, LJ_PREFIX, {default = true}) 48 | collectors_list.gc_steps_finalize_total = 49 | utils.set_counter('gc_steps_finalize_total', 'Total count of incremental GC steps (finalize state)', 50 | lj_metrics.gc_steps_finalize, nil, LJ_PREFIX, {default = true}) 51 | collectors_list.gc_allocated_total = 52 | utils.set_counter('gc_allocated_total', 'Total amount of allocated memory', 53 | lj_metrics.gc_allocated, nil, LJ_PREFIX, {default = true}) 54 | collectors_list.jit_trace_num = 55 | utils.set_gauge('jit_trace_num', 'Amount of JIT traces', 56 | lj_metrics.jit_trace_num, nil, LJ_PREFIX, {default = true}) 57 | collectors_list.gc_steps_sweep_total = 58 | utils.set_counter('gc_steps_sweep_total', 'Total count of incremental GC steps (sweep state)', 59 | lj_metrics.gc_steps_sweep, nil, LJ_PREFIX, {default = true}) 60 | collectors_list.jit_trace_abort_total = 61 | utils.set_counter('jit_trace_abort_total', 'Overall number of abort traces', 62 | lj_metrics.jit_trace_abort, nil, LJ_PREFIX, {default = true}) 63 | collectors_list.jit_mcode_size = 64 | utils.set_gauge('jit_mcode_size', 'Total size of all allocated machine code areas', 65 | lj_metrics.jit_mcode_size, nil, LJ_PREFIX, {default = true}) 66 | collectors_list.gc_steps_propagate_total = 67 | utils.set_counter('gc_steps_propagate_total', 'Total count of incremental GC steps (propagate state)', 68 | lj_metrics.gc_steps_propagate, nil, LJ_PREFIX, {default = true}) 69 | collectors_list.gc_steps_pause_total = 70 | utils.set_counter('gc_steps_pause_total', 'Total count of incremental GC steps (pause state)', 71 | lj_metrics.gc_steps_pause, nil, LJ_PREFIX, {default = true}) 72 | end 73 | 74 | return { 75 | update = update, 76 | list = collectors_list, 77 | } 78 | -------------------------------------------------------------------------------- /metrics/tarantool/memory.lua: -------------------------------------------------------------------------------- 1 | local utils = require('metrics.utils') 2 | 3 | local collectors_list = {} 4 | 5 | local function update_memory_metrics() 6 | if not utils.box_is_configured() then 7 | return 8 | end 9 | 10 | if box.info.memory ~= nil then 11 | local i = box.info.memory() 12 | for k, v in pairs(i) do 13 | local metric_name = 'info_memory_' .. k 14 | collectors_list[metric_name] = utils.set_gauge(metric_name, 'Memory ' .. k, v, 15 | nil, nil, {default = true}) 16 | end 17 | end 18 | end 19 | 20 | return { 21 | update = update_memory_metrics, 22 | list = collectors_list, 23 | } 24 | -------------------------------------------------------------------------------- /metrics/tarantool/operations.lua: -------------------------------------------------------------------------------- 1 | local utils = require('metrics.utils') 2 | 3 | local collectors_list = {} 4 | 5 | local function update_operations_metrics() 6 | if not utils.box_is_configured() then 7 | return 8 | end 9 | 10 | local current_stat = box.stat() 11 | 12 | for k, v in pairs(current_stat) do 13 | collectors_list.stats_op_total = 14 | utils.set_counter('stats_op_total', 'Total amount of operations', 15 | v.total, {operation = k:lower()}, nil, {default = true}) 16 | end 17 | end 18 | 19 | return { 20 | update = update_operations_metrics, 21 | list = collectors_list, 22 | } 23 | -------------------------------------------------------------------------------- /metrics/tarantool/replicas.lua: -------------------------------------------------------------------------------- 1 | local utils = require('metrics.utils') 2 | 3 | local collectors_list = {} 4 | 5 | local function update_replicas_metrics() 6 | if not utils.box_is_configured() then 7 | return 8 | end 9 | 10 | local current_box_info = box.info() 11 | 12 | if current_box_info.ro then 13 | for k, v in pairs(current_box_info.vclock) do 14 | local replication_info = current_box_info.replication[k] 15 | if replication_info then 16 | local lsn = replication_info.lsn 17 | collectors_list.replication_lsn = 18 | utils.set_gauge('replication_lsn', 'lsn for instance', 19 | lsn - v, {type = 'replica', id = k}, nil, {default = true}) 20 | end 21 | end 22 | else 23 | for k, v in pairs(current_box_info.replication) do 24 | if v.downstream ~= nil and v.downstream.vclock ~= nil then 25 | local lsn = v.downstream.vclock[current_box_info.id] 26 | if lsn ~= nil and current_box_info.lsn ~= nil then 27 | collectors_list.replication_lsn = utils.set_gauge( 28 | 'replication_lsn', 29 | 'lsn for instance', 30 | current_box_info.lsn - lsn, 31 | {type = 'master', id = k}, 32 | nil, 33 | {default = true} 34 | ) 35 | end 36 | end 37 | end 38 | end 39 | end 40 | 41 | return { 42 | update = update_replicas_metrics, 43 | list = collectors_list, 44 | } 45 | -------------------------------------------------------------------------------- /metrics/tarantool/runtime.lua: -------------------------------------------------------------------------------- 1 | local utils = require('metrics.utils') 2 | 3 | local collectors_list = {} 4 | 5 | local function update_runtime_metrics() 6 | local runtime_info = box.runtime.info() 7 | 8 | for k, v in pairs(runtime_info) do 9 | if k ~= 'maxalloc' then 10 | local metric_name = 'runtime_' .. k 11 | collectors_list[metric_name] = utils.set_gauge(metric_name, 'Runtime ' .. k, v, 12 | nil, nil, {default = true}) 13 | end 14 | end 15 | end 16 | 17 | return { 18 | update = update_runtime_metrics, 19 | list = collectors_list, 20 | } 21 | -------------------------------------------------------------------------------- /metrics/tarantool/slab.lua: -------------------------------------------------------------------------------- 1 | local utils = require('metrics.utils') 2 | 3 | local collectors_list = {} 4 | 5 | local function update_slab_metrics() 6 | if not utils.box_is_configured() then 7 | return 8 | end 9 | 10 | local slab_info = box.slab.info() 11 | 12 | for k, v in pairs(slab_info) do 13 | local metric_name = 'slab_' .. k 14 | if not k:match('_ratio$') then 15 | collectors_list[metric_name] = utils.set_gauge(metric_name, 'Slab ' .. k .. ' info', v, 16 | nil, nil, {default = true}) 17 | else 18 | collectors_list[metric_name] = 19 | utils.set_gauge(metric_name, 'Slab ' .. k .. ' info', tonumber(v:match('^([0-9%.]+)%%?$')), 20 | nil, nil, {default = true}) 21 | end 22 | end 23 | end 24 | 25 | return { 26 | update = update_slab_metrics, 27 | list = collectors_list, 28 | } 29 | -------------------------------------------------------------------------------- /metrics/tarantool/spaces.lua: -------------------------------------------------------------------------------- 1 | local utils = require('metrics.utils') 2 | 3 | local collectors_list = {} 4 | local spaces = {} 5 | 6 | local function update_spaces_metrics() 7 | if not utils.box_is_configured() then 8 | return 9 | end 10 | 11 | local include_vinyl_count = rawget(_G, 'include_vinyl_count') or false 12 | 13 | local new_spaces = {} 14 | for _, s in box.space._space:pairs() do 15 | local total = 0 16 | local space_name = s[3] 17 | local flags = s[6] 18 | 19 | if s[1] <= box.schema.SYSTEM_ID_MAX or flags.temporary or space_name:match('^_') then 20 | goto continue 21 | end 22 | 23 | local sp = box.space[space_name] 24 | if sp == nil or sp.index[0] == nil then 25 | goto continue 26 | end 27 | 28 | new_spaces[space_name] = {indexes = {}} 29 | 30 | local labels = { name = sp.name } 31 | 32 | for space_id, i in pairs(sp.index) do 33 | if type(space_id) == 'number' then 34 | local l = table.copy(labels) 35 | l.index_name = i.name 36 | collectors_list.space_index_bsize = 37 | utils.set_gauge('space_index_bsize', 'Index bsize', i:bsize(), l, 38 | nil, {default = true}) 39 | total = total + i:bsize() 40 | 41 | if spaces[space_name] ~= nil then 42 | spaces[space_name].indexes[space_id] = nil 43 | end 44 | new_spaces[space_name].indexes[space_id] = l 45 | end 46 | end 47 | 48 | if spaces[space_name] ~= nil then 49 | for _, index in pairs(spaces[space_name].indexes) do 50 | collectors_list.space_index_bsize:remove(index) 51 | end 52 | end 53 | 54 | if sp.engine == 'memtx' then 55 | local sp_bsize = sp:bsize() 56 | 57 | labels.engine = 'memtx' 58 | 59 | collectors_list.space_len = 60 | utils.set_gauge('space_len' , 'Space length', sp:len(), labels, 61 | nil, {default = true}) 62 | 63 | collectors_list.space_bsize = 64 | utils.set_gauge('space_bsize', 'Space bsize', sp_bsize, labels, 65 | nil, {default = true}) 66 | 67 | collectors_list.space_total_bsize = 68 | utils.set_gauge('space_total_bsize', 'Space total bsize', sp_bsize + total, labels, 69 | nil, {default = true}) 70 | new_spaces[space_name].memtx_labels = labels 71 | 72 | spaces[space_name] = nil 73 | else 74 | if include_vinyl_count then 75 | labels.engine = 'vinyl' 76 | local count = sp:count() 77 | collectors_list.vinyl_tuples = 78 | utils.set_gauge('vinyl_tuples', 'Vinyl space tuples count', count, labels, 79 | nil, {default = true}) 80 | new_spaces[space_name].vinyl_labels = labels 81 | 82 | spaces[space_name] = nil 83 | end 84 | end 85 | 86 | ::continue:: 87 | end 88 | 89 | for _, space in pairs(spaces) do 90 | for _, index in pairs(space.indexes) do 91 | collectors_list.space_index_bsize:remove(index) 92 | end 93 | if space.memtx_labels ~= nil then 94 | collectors_list.space_len:remove(space.memtx_labels) 95 | collectors_list.space_bsize:remove(space.memtx_labels) 96 | collectors_list.space_total_bsize:remove(space.memtx_labels) 97 | end 98 | if space.vinyl_labels ~= nil then 99 | collectors_list.vinyl_tuples:remove(space.vinyl_labels) 100 | end 101 | end 102 | spaces = new_spaces 103 | end 104 | 105 | return { 106 | update = update_spaces_metrics, 107 | list = collectors_list, 108 | } 109 | -------------------------------------------------------------------------------- /metrics/tarantool/system.lua: -------------------------------------------------------------------------------- 1 | local utils = require('metrics.utils') 2 | local clock = require('clock') 3 | 4 | local collectors_list = {} 5 | 6 | local function update_system_metrics() 7 | if not utils.box_is_configured() then 8 | return 9 | end 10 | 11 | collectors_list.cfg_current_time = utils.set_gauge('cfg_current_time', 'Tarantool cfg time', clock.time() + 0ULL, 12 | nil, nil, {default = true}) 13 | end 14 | 15 | return { 16 | update = update_system_metrics, 17 | list = collectors_list, 18 | } 19 | -------------------------------------------------------------------------------- /metrics/tarantool/vinyl.lua: -------------------------------------------------------------------------------- 1 | local utils = require('metrics.utils') 2 | 3 | local collectors_list = {} 4 | 5 | local function update() 6 | if not utils.box_is_configured() then 7 | return 8 | end 9 | 10 | local vinyl_stat = box.stat.vinyl() 11 | collectors_list.vinyl_disk_data_size = 12 | utils.set_gauge('vinyl_disk_data_size', 'Amount of data stored in files', 13 | vinyl_stat.disk.data, nil, nil, {default = true}) 14 | collectors_list.vinyl_disk_index_size = 15 | utils.set_gauge('vinyl_disk_index_size', 'Amount of index stored in files', 16 | vinyl_stat.disk.index, nil, nil, {default = true}) 17 | 18 | collectors_list.vinyl_regulator_dump_bandwidth = 19 | utils.set_gauge('vinyl_regulator_dump_bandwidth', 'Estimated average rate at which dumps are done', 20 | vinyl_stat.regulator.dump_bandwidth, nil, nil, {default = true}) 21 | collectors_list.vinyl_regulator_write_rate = 22 | utils.set_gauge('vinyl_regulator_write_rate', 'Average rate at which recent writes to disk are done', 23 | vinyl_stat.regulator.write_rate, nil, nil, {default = true}) 24 | collectors_list.vinyl_regulator_rate_limit = 25 | utils.set_gauge('vinyl_regulator_rate_limit', 'Write rate limit', 26 | vinyl_stat.regulator.rate_limit, nil, nil, {default = true}) 27 | collectors_list.vinyl_regulator_dump_watermark = 28 | utils.set_gauge('vinyl_regulator_dump_watermark', 'Point when dumping must occur', 29 | vinyl_stat.regulator.dump_watermark, nil, nil, {default = true}) 30 | if vinyl_stat.regulator.blocked_writers ~= nil then 31 | collectors_list.vinyl_regulator_blocked_writers = 32 | utils.set_gauge('vinyl_regulator_blocked_writers', 'The number of fibers that are blocked waiting ' .. 33 | 'for Vinyl level0 memory quota', 34 | vinyl_stat.regulator.blocked_writers, nil, nil, {default = true}) 35 | end 36 | 37 | collectors_list.vinyl_tx_conflict = 38 | utils.set_gauge('vinyl_tx_conflict', 'Count of transaction conflicts', 39 | vinyl_stat.tx.conflict, nil, nil, {default = true}) 40 | collectors_list.vinyl_tx_commit = 41 | utils.set_gauge('vinyl_tx_commit', 'Count of commits', 42 | vinyl_stat.tx.commit, nil, nil, {default = true}) 43 | collectors_list.vinyl_tx_rollback = 44 | utils.set_gauge('vinyl_tx_rollback', 'Count of rollbacks', 45 | vinyl_stat.tx.rollback, nil, nil, {default = true}) 46 | collectors_list.vinyl_tx_read_views = 47 | utils.set_gauge('vinyl_tx_read_views', 'Count of open read views', 48 | vinyl_stat.tx.read_views, nil, nil, {default = true}) 49 | 50 | collectors_list.vinyl_memory_tuple_cache = 51 | utils.set_gauge('vinyl_memory_tuple_cache', 'Number of bytes that are being used for tuple', 52 | vinyl_stat.memory.tuple_cache, nil, nil, {default = true}) 53 | collectors_list.vinyl_memory_level0 = 54 | utils.set_gauge('vinyl_memory_level0', 'Size of in-memory storage of an LSM tree', 55 | vinyl_stat.memory.level0, nil, nil, {default = true}) 56 | collectors_list.vinyl_memory_page_index = 57 | utils.set_gauge('vinyl_memory_page_index', 'Size of page indexes', 58 | vinyl_stat.memory.page_index, nil, nil, {default = true}) 59 | collectors_list.vinyl_memory_bloom_filter = 60 | utils.set_gauge('vinyl_memory_bloom_filter', 'Size of bloom filter', 61 | vinyl_stat.memory.bloom_filter, nil, nil, {default = true}) 62 | 63 | if vinyl_stat.memory.tuple ~= nil then 64 | collectors_list.vinyl_memory_tuple = 65 | utils.set_gauge('vinyl_memory_tuple', 66 | 'Total size of memory in bytes occupied by Vinyl tuples', 67 | vinyl_stat.memory.tuple, nil, nil, {default = true}) 68 | end 69 | 70 | collectors_list.vinyl_scheduler_tasks = 71 | utils.set_gauge('vinyl_scheduler_tasks', 'Vinyl tasks count', 72 | vinyl_stat.scheduler.tasks_inprogress, {status = 'inprogress'}, nil, {default = true}) 73 | collectors_list.vinyl_scheduler_tasks = 74 | utils.set_gauge('vinyl_scheduler_tasks', 'Vinyl tasks count', 75 | vinyl_stat.scheduler.tasks_completed, {status = 'completed'}, nil, {default = true}) 76 | collectors_list.vinyl_scheduler_tasks = 77 | utils.set_gauge('vinyl_scheduler_tasks', 'Vinyl tasks count', 78 | vinyl_stat.scheduler.tasks_failed, {status = 'failed'}, nil, {default = true}) 79 | 80 | collectors_list.vinyl_scheduler_dump_time = 81 | utils.set_gauge('vinyl_scheduler_dump_time', 'Total time spent by all worker threads performing dump', 82 | vinyl_stat.scheduler.dump_time, nil, nil, {default = true}) 83 | collectors_list.vinyl_scheduler_dump_total = 84 | utils.set_counter('vinyl_scheduler_dump_total', 'The count of completed dumps', 85 | vinyl_stat.scheduler.dump_count, nil, nil, {default = true}) 86 | end 87 | 88 | return { 89 | update = update, 90 | list = collectors_list, 91 | } 92 | -------------------------------------------------------------------------------- /metrics/utils.lua: -------------------------------------------------------------------------------- 1 | local metrics_api = require('metrics.api') 2 | 3 | local TNT_PREFIX = 'tnt_' 4 | local utils = {} 5 | 6 | function utils.set_gauge(name, description, value, labels, prefix, metainfo) 7 | prefix = prefix or TNT_PREFIX 8 | local gauge = metrics_api.gauge(prefix .. name, description, metainfo) 9 | gauge:set(value, labels or {}) 10 | return gauge 11 | end 12 | 13 | function utils.set_counter(name, description, value, labels, prefix, metainfo) 14 | prefix = prefix or TNT_PREFIX 15 | local counter = metrics_api.counter(prefix .. name, description, metainfo) 16 | counter:reset(labels or {}) 17 | counter:inc(value, labels or {}) 18 | return counter 19 | end 20 | 21 | function utils.box_is_configured() 22 | local is_configured = type(box.cfg) ~= 'function' 23 | if is_configured then 24 | utils.box_is_configured = function() return true end 25 | end 26 | return is_configured 27 | end 28 | 29 | function utils.delete_collectors(list) 30 | if list == nil then 31 | return 32 | end 33 | for _, collector in pairs(list) do 34 | metrics_api.registry:unregister(collector) 35 | end 36 | table.clear(list) 37 | end 38 | 39 | local function get_tarantool_version() 40 | local version_parts = rawget(_G, '_TARANTOOL'):split('-', 3) 41 | 42 | local major_minor_patch_parts = version_parts[1]:split('.', 2) 43 | local major = tonumber(major_minor_patch_parts[1]) 44 | local minor = tonumber(major_minor_patch_parts[2]) 45 | local patch = tonumber(major_minor_patch_parts[3]) 46 | 47 | return major, minor, patch 48 | end 49 | 50 | function utils.is_tarantool3() 51 | local major = get_tarantool_version() 52 | return major == 3 53 | end 54 | 55 | return utils 56 | -------------------------------------------------------------------------------- /metrics/version.lua: -------------------------------------------------------------------------------- 1 | -- Сontains the module version. 2 | -- Requires manual update in case of release commit. 3 | 4 | return '1.3.1' 5 | -------------------------------------------------------------------------------- /rpm/prebuild.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e -o pipefail 4 | 5 | curl -LsSf https://www.tarantool.io/release/1.10/installer.sh | sudo bash 6 | -------------------------------------------------------------------------------- /rpm/tarantool-metrics.spec: -------------------------------------------------------------------------------- 1 | Name: tarantool-metrics 2 | Version: 1.0.0 3 | Release: 1%{?dist} 4 | Summary: Tool to collect metrics with Tarantool 5 | Group: Applications/Databases 6 | License: MIT 7 | URL: https://github.com/tarantool/metrics 8 | Source0: https://github.com/tarantool/%{name}/archive/%{version}/%{name}-%{version}.tar.gz 9 | BuildArch: noarch 10 | Requires: tarantool >= 1.7.5.0 11 | Requires: tarantool-checks >= 2.1.0.0 12 | %description 13 | Easy collecting, storing and manipulating metrics timeseriess. 14 | 15 | %define luapkgdir %{_datadir}/tarantool 16 | %define lualibdir %{_libdir}/tarantool 17 | %define br_luapkgdir %{buildroot}%{luapkgdir} 18 | 19 | %prep 20 | %setup -q -n %{name}-%{version} 21 | 22 | %install 23 | mkdir -p %{br_luapkgdir} 24 | cp -rv metrics %{br_luapkgdir} 25 | mkdir %{br_luapkgdir}/override 26 | cp -rv metrics %{br_luapkgdir}/override 27 | 28 | %files 29 | %{luapkgdir}/metrics 30 | %{luapkgdir}/override 31 | 32 | %doc README.md 33 | %doc doc/monitoring/getting_started.rst 34 | %doc doc/monitoring/api_reference.rst 35 | %doc doc/monitoring/index.rst 36 | %doc doc/monitoring/plugins.rst 37 | 38 | %{!?_licensedir:%global license %doc} 39 | %license LICENSE 40 | 41 | 42 | %changelog 43 | * Tue Jul 09 2019 Albert Sverdlov 1.0.0 44 | - Remove metrics.server 45 | * Thu Mar 07 2019 Elizaveta Dokshina 1.0.0 46 | - Initial version of the RPM spec 47 | -------------------------------------------------------------------------------- /test/collectors/counter_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group() 3 | 4 | local metrics = require('metrics') 5 | local utils = require('test.utils') 6 | 7 | g.after_each(function() 8 | -- Delete all collectors and global labels 9 | metrics.clear() 10 | end) 11 | 12 | g.test_counter = function() 13 | t.assert_error_msg_contains("bad argument #1 to counter (string expected, got nil)", function() 14 | metrics.counter() 15 | end) 16 | 17 | t.assert_error_msg_contains("bad argument #1 to counter (string expected, got number)", function() 18 | metrics.counter(2) 19 | end) 20 | 21 | local c = metrics.counter('cnt', 'some counter') 22 | 23 | c:inc(3) 24 | c:inc(5) 25 | 26 | local collectors = metrics.collectors() 27 | local observations = metrics.collect() 28 | local obs = utils.find_obs('cnt', {}, observations) 29 | t.assert_equals(utils.len(collectors), 1, 'counter seen as only collector') 30 | t.assert_equals(obs.value, 8, '3 + 5 = 8 (via metrics.collectors())') 31 | 32 | t.assert_equals(c:collect()[1].value, 8, '3 + 5 = 8') 33 | 34 | t.assert_error_msg_contains("Counter increment should not be negative", function() 35 | c:inc(-1) 36 | end) 37 | 38 | t.assert_equals(c.dec, nil, "Counter doesn't have 'decrease' method") 39 | 40 | c:inc(0) 41 | t.assert_equals(c:collect()[1].value, 8, '8 + 0 = 8') 42 | end 43 | 44 | g.test_counter_cache = function() 45 | local counter_1 = metrics.counter('cnt', 'test counter') 46 | local counter_2 = metrics.counter('cnt', 'test counter') 47 | local counter_3 = metrics.counter('cnt2', 'another test counter') 48 | 49 | counter_1:inc(3) 50 | counter_2:inc(5) 51 | counter_3:inc(7) 52 | 53 | local collectors = metrics.collectors() 54 | local observations = metrics.collect() 55 | local obs = utils.find_obs('cnt', {}, observations) 56 | t.assert_equals(utils.len(collectors), 2, 'counter_1 and counter_2 refer to the same object') 57 | t.assert_equals(obs.value, 8, '3 + 5 = 8') 58 | obs = utils.find_obs('cnt2', {}, observations) 59 | t.assert_equals(obs.value, 7, 'counter_3 is the only reference to cnt2') 60 | end 61 | 62 | g.test_counter_reset = function() 63 | local c = metrics.counter('cnt', 'some counter') 64 | c:inc() 65 | t.assert_equals(c:collect()[1].value, 1) 66 | c:reset() 67 | t.assert_equals(c:collect()[1].value, 0) 68 | end 69 | 70 | g.test_counter_remove_metric_by_label = function() 71 | local c = metrics.counter('cnt') 72 | 73 | c:inc(1, {label = 1}) 74 | c:inc(1, {label = 2}) 75 | 76 | utils.assert_observations(c:collect(), { 77 | {'cnt', 1, {label = 1}}, 78 | {'cnt', 1, {label = 2}}, 79 | }) 80 | 81 | c:remove({label = 1}) 82 | utils.assert_observations(c:collect(), { 83 | {'cnt', 1, {label = 2}}, 84 | }) 85 | end 86 | 87 | g.test_insert_non_number = function() 88 | local c = metrics.counter('cnt') 89 | t.assert_error_msg_contains('Counter increment should be a number', c.inc, c, true) 90 | end 91 | 92 | g.test_metainfo = function() 93 | local metainfo = {my_useful_info = 'here'} 94 | local c = metrics.counter('cnt', nil, metainfo) 95 | t.assert_equals(c.metainfo, metainfo) 96 | end 97 | 98 | g.test_metainfo_immutable = function() 99 | local metainfo = {my_useful_info = 'here'} 100 | local c = metrics.counter('cnt', nil, metainfo) 101 | metainfo['my_useful_info'] = 'there' 102 | t.assert_equals(c.metainfo, {my_useful_info = 'here'}) 103 | end 104 | -------------------------------------------------------------------------------- /test/collectors/gauge_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group() 3 | 4 | local metrics = require('metrics') 5 | local utils = require('test.utils') 6 | 7 | g.after_each(function() 8 | -- Delete all collectors and global labels 9 | metrics.clear() 10 | end) 11 | 12 | g.test_gauge = function() 13 | t.assert_error_msg_contains("bad argument #1 to gauge (string expected, got nil)", function() 14 | metrics.gauge() 15 | end) 16 | 17 | t.assert_error_msg_contains("bad argument #1 to gauge (string expected, got number)", function() 18 | metrics.gauge(2) 19 | end) 20 | 21 | local gauge = metrics.gauge('gauge', 'some gauge') 22 | 23 | gauge:inc(3) 24 | gauge:dec(5) 25 | 26 | local collectors = metrics.collectors() 27 | local observations = metrics.collect() 28 | local obs = utils.find_obs('gauge', {}, observations) 29 | t.assert_equals(utils.len(collectors), 1, 'gauge seen as only collector') 30 | t.assert_equals(obs.value, -2, '3 - 5 = -2 (via metrics.collectors())') 31 | 32 | t.assert_equals(gauge:collect()[1].value, -2, '3 - 5 = -2') 33 | 34 | gauge:set(-8) 35 | 36 | t.assert_equals(gauge:collect()[1].value, -8, 'after set(-8) = -8') 37 | 38 | gauge:inc(-1) 39 | gauge:dec(-2) 40 | 41 | t.assert_equals(gauge:collect()[1].value, -7, '-8 + (-1) - (-2)') 42 | end 43 | 44 | g.test_gauge_remove_metric_by_label = function() 45 | local c = metrics.gauge('gauge') 46 | 47 | c:set(1, {label = 1}) 48 | c:set(1, {label = 2}) 49 | 50 | utils.assert_observations(c:collect(), { 51 | {'gauge', 1, {label = 1}}, 52 | {'gauge', 1, {label = 2}}, 53 | }) 54 | 55 | c:remove({label = 1}) 56 | utils.assert_observations(c:collect(), { 57 | {'gauge', 1, {label = 2}}, 58 | }) 59 | end 60 | 61 | g.test_inc_non_number = function() 62 | local c = metrics.gauge('gauge') 63 | 64 | t.assert_error_msg_contains('Collector increment should be a number', c.inc, c, true) 65 | end 66 | 67 | g.test_dec_non_number = function() 68 | local c = metrics.gauge('gauge') 69 | 70 | t.assert_error_msg_contains('Collector decrement should be a number', c.dec, c, true) 71 | end 72 | 73 | g.test_inc_non_number = function() 74 | local c = metrics.gauge('gauge') 75 | 76 | t.assert_error_msg_contains('Collector set value should be a number', c.set, c, true) 77 | end 78 | 79 | g.test_metainfo = function() 80 | local metainfo = {my_useful_info = 'here'} 81 | local c = metrics.gauge('gauge', nil, metainfo) 82 | t.assert_equals(c.metainfo, metainfo) 83 | end 84 | 85 | g.test_metainfo_immutable = function() 86 | local metainfo = {my_useful_info = 'here'} 87 | local c = metrics.gauge('gauge', nil, metainfo) 88 | metainfo['my_useful_info'] = 'there' 89 | t.assert_equals(c.metainfo, {my_useful_info = 'here'}) 90 | end 91 | -------------------------------------------------------------------------------- /test/collectors/histogram_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group() 3 | 4 | local metrics = require('metrics') 5 | local Histogram = require('metrics.collectors.histogram') 6 | local utils = require('test.utils') 7 | 8 | g.before_all(utils.create_server) 9 | g.after_all(utils.drop_server) 10 | 11 | g.before_each(metrics.clear) 12 | 13 | g.test_unsorted_buckets_error = function() 14 | t.assert_error_msg_contains('Invalid value for buckets', metrics.histogram, 'latency', nil, {0.9, 0.5}) 15 | end 16 | 17 | g.test_remove_metric_by_label = function() 18 | local h = Histogram:new('hist', 'some histogram', {2, 4}) 19 | 20 | h:observe(3, {label = 1}) 21 | h:observe(5, {label = 2}) 22 | 23 | utils.assert_observations(h:collect(), 24 | { 25 | {'hist_count', 1, {label = 1}}, 26 | {'hist_count', 1, {label = 2}}, 27 | {'hist_sum', 3, {label = 1}}, 28 | {'hist_sum', 5, {label = 2}}, 29 | {'hist_bucket', 0, {label = 1, le = 2}}, 30 | {'hist_bucket', 1, {label = 1, le = math.huge}}, 31 | {'hist_bucket', 1, {label = 1, le = 4}}, 32 | {'hist_bucket', 0, {label = 2, le = 4}}, 33 | {'hist_bucket', 1, {label = 2, le = math.huge}}, 34 | {'hist_bucket', 0, {label = 2, le = 2}}, 35 | } 36 | ) 37 | 38 | h:remove({label = 1}) 39 | utils.assert_observations(h:collect(), 40 | { 41 | {'hist_count', 1, {label = 2}}, 42 | {'hist_sum', 5, {label = 2}}, 43 | {'hist_bucket', 0, {label = 2, le = 4}}, 44 | {'hist_bucket', 1, {label = 2, le = math.huge}}, 45 | {'hist_bucket', 0, {label = 2, le = 2}}, 46 | } 47 | ) 48 | end 49 | 50 | g.test_histogram = function() 51 | t.assert_error_msg_contains("bad argument #1 to histogram (string expected, got nil)", function() 52 | metrics.histogram() 53 | end) 54 | 55 | t.assert_error_msg_contains("bad argument #1 to histogram (string expected, got number)", function() 56 | metrics.histogram(2) 57 | end) 58 | 59 | local h = metrics.histogram('hist', 'some histogram', {2, 4}) 60 | 61 | h:observe(3) 62 | h:observe(5) 63 | 64 | local collectors = metrics.collectors() 65 | t.assert_equals(utils.len(collectors), 1, 'histogram seen as only 1 collector') 66 | local observations = metrics.collect() 67 | local obs_sum = utils.find_obs('hist_sum', {}, observations) 68 | local obs_count = utils.find_obs('hist_count', {}, observations) 69 | local obs_bucket_2 = utils.find_obs('hist_bucket', { le = 2 }, observations) 70 | local obs_bucket_4 = utils.find_obs('hist_bucket', { le = 4 }, observations) 71 | local obs_bucket_inf = utils.find_obs('hist_bucket', { le = metrics.INF }, observations) 72 | t.assert_equals(#observations, 5, '_sum, _count, and _bucket with 3 labelpairs') 73 | t.assert_equals(obs_sum.value, 8, '3 + 5 = 8') 74 | t.assert_equals(obs_count.value, 2, '2 observed values') 75 | t.assert_equals(obs_bucket_2.value, 0, 'bucket 2 has no values') 76 | t.assert_equals(obs_bucket_4.value, 1, 'bucket 4 has 1 value: 3') 77 | t.assert_equals(obs_bucket_inf.value, 2, 'bucket +inf has 2 values: 3, 5') 78 | 79 | h:observe(3, { foo = 'bar' }) 80 | 81 | collectors = metrics.collectors() 82 | t.assert_equals(utils.len(collectors), 1, 'still histogram seen as only 1 collector') 83 | observations = metrics.collect() 84 | obs_sum = utils.find_obs('hist_sum', { foo = 'bar' }, observations) 85 | obs_count = utils.find_obs('hist_count', { foo = 'bar' }, observations) 86 | obs_bucket_2 = utils.find_obs('hist_bucket', { le = 2, foo = 'bar' }, observations) 87 | obs_bucket_4 = utils.find_obs('hist_bucket', { le = 4, foo = 'bar' }, observations) 88 | obs_bucket_inf = utils.find_obs('hist_bucket', { le = metrics.INF, foo = 'bar' }, observations) 89 | 90 | t.assert_equals(#observations, 10, '+ _sum, _count, and _bucket with 3 labelpairs') 91 | t.assert_equals(obs_sum.value, 3, '3 = 3') 92 | t.assert_equals(obs_count.value, 1, '1 observed values') 93 | t.assert_equals(obs_bucket_2.value, 0, 'bucket 2 has no values') 94 | t.assert_equals(obs_bucket_4.value, 1, 'bucket 4 has 1 value: 3') 95 | t.assert_equals(obs_bucket_inf.value, 1, 'bucket +inf has 1 value: 3') 96 | end 97 | 98 | g.test_insert_non_number = function() 99 | local h = metrics.histogram('hist', 'some histogram', {2, 4}) 100 | 101 | t.assert_error_msg_contains('Histogram observation should be a number', h.observe, h, true) 102 | end 103 | 104 | g.test_insert_cdata = function(cg) 105 | cg.server:exec(function() 106 | local h = require('metrics').histogram('hist', 'some histogram', {2, 4}) 107 | t.assert_not(h:observe(0ULL)) 108 | end) 109 | 110 | local warn = "Using cdata as observation in historgam " .. 111 | "can lead to unexpected results. " .. 112 | "That log message will be an error in the future." 113 | t.assert_not_equals(cg.server:grep_log(warn), nil) 114 | end 115 | 116 | g.test_metainfo = function() 117 | local metainfo = {my_useful_info = 'here'} 118 | local h = metrics.histogram('hist', 'some histogram', {2, 4}, metainfo) 119 | t.assert_equals(h.metainfo, metainfo) 120 | t.assert_equals(h.sum_collector.metainfo, metainfo) 121 | t.assert_equals(h.count_collector.metainfo, metainfo) 122 | t.assert_equals(h.bucket_collector.metainfo, metainfo) 123 | end 124 | 125 | g.test_metainfo_immutable = function() 126 | local metainfo = {my_useful_info = 'here'} 127 | local h = metrics.histogram('hist', 'some histogram', {2, 4}, metainfo) 128 | metainfo['my_useful_info'] = 'there' 129 | t.assert_equals(h.metainfo, {my_useful_info = 'here'}) 130 | t.assert_equals(h.sum_collector.metainfo, {my_useful_info = 'here'}) 131 | t.assert_equals(h.count_collector.metainfo, {my_useful_info = 'here'}) 132 | t.assert_equals(h.bucket_collector.metainfo, {my_useful_info = 'here'}) 133 | end 134 | -------------------------------------------------------------------------------- /test/collectors/latency_observer_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group('latency_observer') 3 | 4 | local shared = require('metrics.collectors.shared') 5 | 6 | local clock = require('clock') 7 | local fiber = require('fiber') 8 | 9 | local function stub_observer(observe) 10 | return { 11 | observe = observe or function(...) return ... end, 12 | observe_latency = shared.observe_latency, 13 | } 14 | end 15 | 16 | g.before_all(function() 17 | g.observer = stub_observer() 18 | end) 19 | 20 | local function sleep(time) 21 | local start_time = clock.monotonic() 22 | while (clock.monotonic() - start_time) < time do 23 | fiber.yield() 24 | end 25 | end 26 | 27 | local function work_fn(work_time, ...) 28 | sleep(work_time) 29 | return ... 30 | end 31 | 32 | local function work_fn_raise_error(work_time) 33 | sleep(work_time) 34 | error('caused error') 35 | end 36 | 37 | g.test_measured_fn_return_result = function() 38 | local result = {status = 200} 39 | t.assert_equals( 40 | g.observer:observe_latency({name = 'echo'}, work_fn, 0.0, result), 41 | result 42 | ) 43 | end 44 | 45 | g.test_measured_fn_raise_error = function() 46 | t.assert_error_msg_content_equals( 47 | 'caused error', 48 | function() 49 | return g.observer:observe_latency({name = 'raise_error'}, work_fn_raise_error, 0.0) 50 | end 51 | ) 52 | end 53 | 54 | g.test_measured_fn_return_error = function() 55 | local ok, err = g.observer:observe_latency({name = 'return_error'}, work_fn, 0.0, nil, 'returned error') 56 | t.assert_equals(ok, nil) 57 | t.assert_equals(err, 'returned error') 58 | end 59 | 60 | g.test_dynamic_label_pairs = function() 61 | local obs = nil 62 | local observer = stub_observer(function(_, _, label_pairs) 63 | obs = {label_pairs = label_pairs} 64 | end) 65 | 66 | local label_pairs_gen_fn = function(ok, _, _) return {status=tostring(ok)} end 67 | 68 | observer:observe_latency(label_pairs_gen_fn, work_fn, 0.0) 69 | t.assert_equals(obs.label_pairs, {status = 'true'}) 70 | 71 | pcall(function() observer:observe_latency(label_pairs_gen_fn, work_fn_raise_error, 0.0) end) 72 | t.assert_equals(obs.label_pairs, {status = 'false'}) 73 | end 74 | 75 | g.test_time_measurement = function() 76 | local obs = nil 77 | local observer = stub_observer(function(_, value, _) obs = {value = value} end) 78 | 79 | local work_times = {0.3, 0.2, 0.1} 80 | for _, time in ipairs(work_times) do 81 | local start_time = clock.monotonic() 82 | observer:observe_latency({}, work_fn, time) 83 | local finish_time = clock.monotonic() 84 | t.assert_ge(finish_time - start_time, obs.value) 85 | t.assert_le(time, obs.value) 86 | end 87 | end 88 | 89 | g.test_time_measurement_with_error = function() 90 | local obs = nil 91 | local observer = stub_observer(function(_, value, label_pairs) 92 | obs = {value = value, label_pairs = label_pairs} 93 | end) 94 | 95 | local work_times = {0.3, 0.2, 0.1} 96 | for _, time in ipairs(work_times) do 97 | local start_time = clock.monotonic() 98 | pcall(function() observer:observe_latency( 99 | function(ok, _, _) return {status = tostring(ok)} end, 100 | work_fn_raise_error, 101 | time 102 | ) end) 103 | local finish_time = clock.monotonic() 104 | t.assert_equals(obs.label_pairs, {status = 'false'}) 105 | t.assert_ge(finish_time - start_time, obs.value) 106 | t.assert_le(time, obs.value) 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /test/collectors/shared_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group() 3 | 4 | local utils = require('test.utils') 5 | local Shared = require('metrics.collectors.shared') 6 | 7 | g.test_different_order_in_label_pairs = function() 8 | local class = Shared:new_class('test_class', {'inc'}) 9 | local collector = class:new('test') 10 | collector:inc(1, {a = 1, b = 2}) 11 | collector:inc(1, {a = 2, b = 1}) 12 | collector:inc(1, {b = 2, a = 1}) 13 | utils.assert_observations(collector:collect(), { 14 | {'test', 2, {a = 1, b = 2}}, 15 | {'test', 1, {a = 2, b = 1}}, 16 | }) 17 | end 18 | 19 | g.test_remove_metric_by_label = function() 20 | local class = Shared:new_class('test_class', {'inc'}) 21 | local collector = class:new('test') 22 | collector:inc(1, {a = 1, b = 2}) 23 | collector:inc(1, {a = 2, b = 1}) 24 | utils.assert_observations(collector:collect(), { 25 | {'test', 1, {a = 1, b = 2}}, 26 | {'test', 1, {a = 2, b = 1}}, 27 | }) 28 | collector:remove({a = 2, b = 1}) 29 | utils.assert_observations(collector:collect(), { 30 | {'test', 1, {a = 1, b = 2}}, 31 | }) 32 | end 33 | 34 | g.test_metainfo = function() 35 | local metainfo = {my_useful_info = 'here'} 36 | local c = Shared:new('collector', nil, metainfo) 37 | t.assert_equals(c.metainfo, metainfo) 38 | end 39 | 40 | 41 | g.test_metainfo_immutable = function() 42 | local metainfo = {my_useful_info = 'here'} 43 | local c = Shared:new('collector', nil, metainfo) 44 | metainfo['my_useful_info'] = 'there' 45 | t.assert_equals(c.metainfo, {my_useful_info = 'here'}) 46 | end 47 | -------------------------------------------------------------------------------- /test/entrypoint/srv_basic.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | require('strict').on() 4 | 5 | local log = require('log') 6 | local errors = require('errors') 7 | local cartridge = require('cartridge') 8 | errors.set_deprecation_handler(function(err) 9 | log.error('%s', err) 10 | os.exit(1) 11 | end) 12 | 13 | local ok, err = errors.pcall('CartridgeCfgError', cartridge.cfg, { 14 | roles = {}, 15 | roles_reload_allowed = os.getenv('TARANTOOL_ROLES_RELOAD_ALLOWED') == 'true' or nil, 16 | }) 17 | if not ok then 18 | log.error('%s', err) 19 | os.exit(1) 20 | end 21 | 22 | local metrics = require('metrics') 23 | metrics.cfg({ 24 | include = { 25 | 'cartridge_issues', 26 | 'cartridge_failover', 27 | 'info', 28 | 'clock', 29 | } 30 | }) 31 | 32 | local httpd = cartridge.service_get('httpd') 33 | 34 | httpd:route({ 35 | method = 'GET', 36 | name = '/metrics', 37 | path = '/metrics' 38 | }, function(req) 39 | local json_exporter = require('metrics.plugins.json') 40 | return req:render({ text = json_exporter.export() }) 41 | end) 42 | -------------------------------------------------------------------------------- /test/helper.lua: -------------------------------------------------------------------------------- 1 | require('strict').on() 2 | 3 | -- Override should be preferred over built-in. 4 | require('test.rock_utils').assert_nonbuiltin('metrics') 5 | 6 | 7 | local fio = require('fio') 8 | local utils = require('test.utils') 9 | 10 | local root = fio.dirname(fio.dirname(fio.abspath(package.search('test.utils')))) 11 | 12 | package.loaded['test.utils'].LUA_PATH = root .. '/?.lua;' .. 13 | root .. '/?/init.lua;' .. 14 | root .. '/.rocks/share/tarantool/?.lua;' .. 15 | root .. '/.rocks/share/tarantool/?/init.lua' 16 | 17 | local t = require('luatest') 18 | local ok, cartridge_helpers = pcall(require, 'cartridge.test-helpers') 19 | if not ok then 20 | return nil 21 | end 22 | local cartridge_version = require('cartridge.VERSION') 23 | local helpers = table.copy(cartridge_helpers) 24 | 25 | helpers.project_root = fio.dirname(debug.sourcedir()) 26 | 27 | function helpers.entrypoint(name) 28 | local path = fio.pathjoin( 29 | helpers.project_root, 30 | 'test', 'entrypoint', 31 | string.format('%s.lua', name) 32 | ) 33 | if not fio.path.exists(path) then 34 | error(path .. ': no such entrypoint', 2) 35 | end 36 | return path 37 | end 38 | 39 | function helpers.init_cluster(env) 40 | local cluster = helpers.Cluster:new({ 41 | datadir = fio.tempdir(), 42 | server_command = helpers.entrypoint('srv_basic'), 43 | replicasets = { 44 | { 45 | uuid = helpers.uuid('a'), 46 | roles = {}, 47 | servers = { 48 | {instance_uuid = helpers.uuid('a', 1), alias = 'main'}, 49 | {instance_uuid = helpers.uuid('b', 1), alias = 'replica'}, 50 | }, 51 | }, 52 | }, 53 | env = env, 54 | }) 55 | cluster:start() 56 | return cluster 57 | end 58 | 59 | function helpers.upload_default_metrics_config(cluster) 60 | cluster:upload_config({ 61 | metrics = { 62 | export = { 63 | { 64 | path = '/health', 65 | format = 'health' 66 | }, 67 | { 68 | path = '/metrics', 69 | format = 'json' 70 | }, 71 | }, 72 | } 73 | }) 74 | end 75 | 76 | function helpers.set_export(cluster, export) 77 | local server = cluster.main_server 78 | return server.net_box:eval([[ 79 | local cartridge = require('cartridge') 80 | local metrics = cartridge.service_get('metrics') 81 | local _, err = pcall( 82 | metrics.set_export, ... 83 | ) 84 | return err 85 | ]], {export}) 86 | end 87 | 88 | function helpers.skip_cartridge_version_less(version) 89 | t.skip_if(cartridge_version == 'unknown', 'Cartridge version is unknown, must be v' .. version .. ' or greater') 90 | t.skip_if(utils.is_version_less(cartridge_version, version), 91 | 'Cartridge version is must be v' .. version .. ' or greater') 92 | end 93 | 94 | function helpers.skip_cartridge_version_greater(version) 95 | t.skip_if(cartridge_version == 'unknown', 'Cartridge version is unknown, must be v' .. version .. ' or less') 96 | t.skip_if(utils.is_version_greater(cartridge_version, version), 97 | 'Cartridge version is must be v' .. version .. ' or less') 98 | end 99 | 100 | return helpers 101 | -------------------------------------------------------------------------------- /test/integration/cartridge_metrics_test.lua: -------------------------------------------------------------------------------- 1 | local fio = require('fio') 2 | local t = require('luatest') 3 | local g = t.group() 4 | 5 | local utils = require('test.utils') 6 | local helpers = require('test.helper') 7 | 8 | g.before_each(function() 9 | t.skip_if(type(helpers) ~= 'table', 'Skip cartridge test') 10 | g.cluster = helpers.init_cluster() 11 | helpers.upload_default_metrics_config(g.cluster) 12 | end) 13 | 14 | g.after_each(function() 15 | g.cluster:stop() 16 | fio.rmtree(g.cluster.datadir) 17 | end) 18 | 19 | g.test_cartridge_issues_present_on_healthy_cluster = function() 20 | helpers.skip_cartridge_version_less('2.0.2') 21 | local main_server = g.cluster:server('main') 22 | local resp = main_server:http_request('get', '/metrics') 23 | local issues_metric = utils.find_metric('tnt_cartridge_issues', resp.json) 24 | t.assert_is_not(issues_metric, nil, 'Cartridge issues metric presents in /metrics response') 25 | 26 | t.helpers.retrying({}, function() 27 | resp = main_server:http_request('get', '/metrics') 28 | issues_metric = utils.find_metric('tnt_cartridge_issues', resp.json) 29 | for _, v in ipairs(issues_metric) do 30 | t.assert_equals(v.value, 0, 'Issues count is zero cause new-built cluster should be healthy') 31 | end 32 | end) 33 | end 34 | 35 | g.test_cartridge_issues_metric_warning = function() 36 | helpers.skip_cartridge_version_less('2.0.2') 37 | helpers.skip_cartridge_version_greater('2.5.0') 38 | local main_server = g.cluster:server('main') 39 | local replica_server = g.cluster:server('replica') 40 | 41 | -- Stage replication issue "Duplicate key exists in unique index 'primary' in space '_space'" 42 | main_server.net_box:eval([[ 43 | __replication = box.cfg.replication 44 | box.cfg{replication = box.NULL} 45 | ]]) 46 | replica_server.net_box:eval([[ 47 | box.cfg{read_only = false} 48 | box.schema.space.create('test') 49 | ]]) 50 | main_server.net_box:eval([[ 51 | box.schema.space.create('test') 52 | pcall(box.cfg, {replication = __replication}) 53 | __replication = nil 54 | ]]) 55 | 56 | t.helpers.retrying({}, function() 57 | local resp = main_server:http_request('get', '/metrics') 58 | local issues_metric = utils.find_metric('tnt_cartridge_issues', resp.json)[1] 59 | t.assert_equals(issues_metric.value, 1, [[ 60 | Issues count is one cause main instance should has one replication issue: 61 | replication from main to replica is stopped. 62 | ]]) 63 | t.assert_equals(issues_metric.label_pairs.level, 'warning') 64 | end) 65 | end 66 | 67 | g.test_cartridge_issues_metric_critical = function() 68 | helpers.skip_cartridge_version_less('2.0.2') 69 | local main_server = g.cluster:server('main') 70 | 71 | main_server.net_box:eval([[ 72 | box.slab.info = function() 73 | return { 74 | items_used = 99, 75 | items_size = 100, 76 | arena_used = 99, 77 | arena_size = 100, 78 | quota_used = 99, 79 | quota_size = 100, 80 | } 81 | end 82 | ]]) 83 | 84 | t.helpers.retrying({}, function() 85 | local resp = main_server:http_request('get', '/metrics') 86 | local issues_metric = utils.find_metric('tnt_cartridge_issues', resp.json)[2] 87 | t.assert_equals(issues_metric.value, 1) 88 | t.assert_equals(issues_metric.label_pairs.level, 'critical') 89 | end) 90 | end 91 | 92 | g.test_clock_delta_metric_present = function() 93 | local main_server = g.cluster:server('main') 94 | 95 | t.helpers.retrying({}, function() 96 | local resp = main_server:http_request('get', '/metrics') 97 | local clock_delta_metrics = utils.find_metric('tnt_clock_delta', resp.json) 98 | t.assert_equals(#clock_delta_metrics, 2) 99 | t.assert_equals(clock_delta_metrics[1].label_pairs.delta, 'max') 100 | t.assert_equals(clock_delta_metrics[2].label_pairs.delta, 'min') 101 | end) 102 | end 103 | 104 | g.test_read_only = function() 105 | local main_server = g.cluster:server('main') 106 | 107 | local resp = main_server:http_request('get', '/metrics') 108 | local read_only = utils.find_metric('tnt_read_only', resp.json) 109 | t.assert_equals(read_only[1].value, 0) 110 | 111 | local replica_server = g.cluster:server('replica') 112 | resp = replica_server:http_request('get', '/metrics') 113 | read_only = utils.find_metric('tnt_read_only', resp.json) 114 | t.assert_equals(read_only[1].value, 1) 115 | end 116 | 117 | g.test_failover = function() 118 | helpers.skip_cartridge_version_less('2.7.5') 119 | g.cluster:wait_until_healthy() 120 | g.cluster.main_server:graphql({ 121 | query = [[ 122 | mutation($mode: String) { 123 | cluster { 124 | failover_params( 125 | mode: $mode 126 | ) { 127 | mode 128 | } 129 | } 130 | } 131 | ]], 132 | variables = { mode = 'eventual' }, 133 | raise = false, 134 | }) 135 | g.cluster:wait_until_healthy() 136 | g.cluster.main_server:stop() 137 | 138 | helpers.retrying({timeout = 30}, function() 139 | local resp = g.cluster:server('replica'):http_request('get', '/metrics') 140 | local failover_trigger_cnt = utils.find_metric('tnt_cartridge_failover_trigger_total', resp.json) 141 | t.assert_equals(failover_trigger_cnt[1].value, 1) 142 | end) 143 | 144 | g.cluster.main_server:start() 145 | g.cluster:wait_until_healthy() 146 | end 147 | -------------------------------------------------------------------------------- /test/integration/highload_test.lua: -------------------------------------------------------------------------------- 1 | require('strict').on() 2 | 3 | local t = require('luatest') 4 | local g = t.group('highload') 5 | local utils = require('test.utils') 6 | 7 | g.before_all(utils.create_server) 8 | 9 | g.after_all(utils.drop_server) 10 | 11 | g.test_eventloop = function(cg) 12 | cg.server:exec(function() 13 | local metrics = require('metrics') 14 | local fiber = require('fiber') 15 | local clock = require('clock') 16 | local utils = require('test.utils') -- luacheck: ignore 431 17 | 18 | local function monitor(collector) 19 | local time_before 20 | while true do 21 | time_before = clock.monotonic() 22 | fiber.yield() 23 | collector:observe(clock.monotonic() - time_before) 24 | end 25 | end 26 | 27 | metrics.set_global_labels({ alias = 'my_instance' }) 28 | 29 | local collector = metrics.summary('tnt_fiber_event_loop', 'event loop time', 30 | { [0.5] = 0.01, [0.9] = 0.01, [0.99] = 0.01, }) 31 | local fiber_object = fiber.create(function() monitor(collector) end) 32 | 33 | for _ = 1, 10 do 34 | fiber.sleep(0.1) 35 | local observations = metrics.collect() 36 | 37 | local obs_summary = utils.find_obs('tnt_fiber_event_loop', 38 | { alias = 'my_instance', quantile = 0.99 }, observations) 39 | 40 | t.assert_not_inf(obs_summary.value) 41 | end 42 | 43 | fiber.kill(fiber_object:id()) 44 | end) 45 | end 46 | -------------------------------------------------------------------------------- /test/integration/hotreload_test.lua: -------------------------------------------------------------------------------- 1 | require('strict').on() 2 | 3 | local t = require('luatest') 4 | local g = t.group('hotreload') 5 | 6 | local utils = require('test.utils') 7 | 8 | g.before_all(function(cg) 9 | utils.create_server(cg) 10 | cg.server:exec(function() 11 | local function package_reload() 12 | for k, _ in pairs(package.loaded) do 13 | if k:find('metrics') ~= nil then 14 | package.loaded[k] = nil 15 | end 16 | end 17 | 18 | return require('metrics') 19 | end 20 | 21 | rawset(_G, 'package_reload', package_reload) 22 | end) 23 | end) 24 | 25 | g.after_all(utils.drop_server) 26 | 27 | g.test_reload = function(cg) 28 | cg.server:exec(function() 29 | local metrics = require('metrics') 30 | 31 | local http_requests_total_counter = metrics.counter('http_requests_total') 32 | http_requests_total_counter:inc(1, {method = 'GET'}) 33 | 34 | local http_requests_latency = metrics.summary( 35 | 'http_requests_latency', 'HTTP requests total', 36 | {[0.5]=0.01, [0.9]=0.01, [0.99]=0.01} 37 | ) 38 | http_requests_latency:observe(10) 39 | 40 | metrics.enable_default_metrics() 41 | 42 | metrics = rawget(_G, 'package_reload')() 43 | 44 | metrics.enable_default_metrics() 45 | end) 46 | end 47 | 48 | g.test_cartridge_hotreload_preserves_cfg_state = function(cg) 49 | cg.server:exec(function() 50 | local metrics = require('metrics') 51 | local utils = require('test.utils') -- luacheck: ignore 431 52 | 53 | local cfg_before_hotreload = metrics.cfg{include = {'operations'}} 54 | local obs_before_hotreload = metrics.collect{invoke_callbacks = true} 55 | 56 | metrics = rawget(_G, 'package_reload')() 57 | 58 | local cfg_after_hotreload = metrics.cfg 59 | box.space._space:select(nil, {limit = 1}) 60 | local obs_after_hotreload = metrics.collect{invoke_callbacks = true} 61 | 62 | t.assert_equals(cfg_before_hotreload, cfg_after_hotreload, 63 | "cfg values are preserved") 64 | 65 | local op_before = utils.find_obs('tnt_stats_op_total', {operation = 'select'}, 66 | obs_before_hotreload, t.assert_covers) 67 | local op_after = utils.find_obs('tnt_stats_op_total', {operation = 'select'}, 68 | obs_after_hotreload, t.assert_covers) 69 | t.assert_gt(op_after.value, op_before.value, "metric callbacks enabled by cfg stay enabled") 70 | end) 71 | end 72 | -------------------------------------------------------------------------------- /test/plugins/graphite_test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | require('strict').on() 4 | 5 | local t = require('luatest') 6 | local g = t.group() 7 | 8 | local socket = require('socket') 9 | local utils = require('test.utils') 10 | 11 | g.before_all(function(cg) 12 | utils.create_server(cg) 13 | cg.server:exec(function() 14 | local metrics = require('metrics') 15 | 16 | local s = box.schema.space.create( 17 | 'random_space_for_graphite', 18 | {if_not_exists = true}) 19 | s:create_index('pk', {if_not_exists = true}) 20 | 21 | local s_vinyl = box.schema.space.create( 22 | 'random_vinyl_space_for_graphite', 23 | {if_not_exists = true, engine = 'vinyl'}) 24 | s_vinyl:create_index('pk', {if_not_exists = true}) 25 | 26 | -- Enable default metrics collections 27 | metrics.enable_default_metrics() 28 | end) 29 | end) 30 | 31 | g.after_all(utils.drop_server) 32 | 33 | g.before_each(function(cg) 34 | cg.server:exec(function() require('metrics').clear() end) 35 | end) 36 | 37 | g.after_each(function(cg) 38 | cg.server:exec(function() 39 | local fiber = require('fiber') 40 | local fun = require('fun') 41 | local metrics = require('metrics') 42 | -- Delete all collectors and global labels 43 | metrics.clear() 44 | fun.iter(fiber.info()): 45 | filter(function(_, x) return x.name == 'metrics_graphite_worker' end): 46 | each(function(x) fiber.kill(x) end) 47 | fiber.yield() -- let cancelled fibers disappear from fiber.info() 48 | end) 49 | end) 50 | 51 | g.test_graphite_format_observation_removes_ULL_suffix = function(cg) 52 | cg.server:exec(function() 53 | local metrics = require('metrics') 54 | local graphite = require('metrics.plugins.graphite') 55 | 56 | local gauge = metrics.gauge('test_gauge', 'Test gauge') 57 | gauge:set(1ULL) 58 | local obs = gauge:collect()[1] 59 | local ull_number = tostring(obs.value) 60 | t.assert_equals(ull_number, '1ULL') 61 | 62 | local graphite_obs = graphite.format_observation('', obs) 63 | 64 | local graphite_val = graphite_obs:split(' ')[2] 65 | t.assert_equals(graphite_val, '1') 66 | end) 67 | end 68 | 69 | g.test_graphite_format_observation_removes_LL_suffix = function(cg) 70 | cg.server:exec(function() 71 | local metrics = require('metrics') 72 | local graphite = require('metrics.plugins.graphite') 73 | 74 | local gauge = metrics.gauge('test_gauge', 'Test gauge') 75 | gauge:set(1LL) 76 | local obs = gauge:collect()[1] 77 | local ll_number = tostring(obs.value) 78 | t.assert_equals(ll_number, '1LL') 79 | 80 | local graphite_obs = graphite.format_observation('', obs) 81 | 82 | local graphite_val = graphite_obs:split(' ')[2] 83 | t.assert_equals(graphite_val, '1') 84 | end) 85 | end 86 | 87 | g.test_graphite_format_observation_float = function(cg) 88 | cg.server:exec(function() 89 | local metrics = require('metrics') 90 | local graphite = require('metrics.plugins.graphite') 91 | 92 | local gauge = metrics.gauge('test_gauge', 'Test gauge') 93 | gauge:set(1.5) 94 | local obs = gauge:collect()[1] 95 | local ll_number = tostring(obs.value) 96 | t.assert_equals(ll_number, '1.5') 97 | 98 | local graphite_obs = graphite.format_observation('', obs) 99 | 100 | local graphite_val = graphite_obs:split(' ')[2] 101 | t.assert_equals(graphite_val, '1.5') 102 | end) 103 | end 104 | 105 | g.test_graphite_format_observation_time_in_seconds = function(cg) 106 | cg.server:exec(function() 107 | local fiber = require('fiber') 108 | local graphite = require('metrics.plugins.graphite') 109 | 110 | local obs = { 111 | metric_name = 'test_metric', 112 | label_pairs = {}, 113 | value = 1, 114 | timestamp = fiber.time64(), 115 | } 116 | 117 | local graphite_obs = graphite.format_observation('', obs) 118 | 119 | local graphite_time = tonumber(graphite_obs:split(' ')[3]) 120 | local sec = obs.timestamp / 10^6 121 | t.assert_equals(graphite_time, sec) 122 | end) 123 | end 124 | 125 | g.test_graphite_format_observation_signed_time = function(cg) 126 | cg.server:exec(function() 127 | local graphite = require('metrics.plugins.graphite') 128 | 129 | local obs = { 130 | metric_name = 'test_metric', 131 | label_pairs = {}, 132 | value = 1, 133 | timestamp = 1000000000000LL, 134 | } 135 | 136 | local graphite_obs = graphite.format_observation('', obs) 137 | 138 | local graphite_time = tonumber(graphite_obs:split(' ')[3]) 139 | local sec = obs.timestamp / 10^6 140 | t.assert_equals(graphite_time, sec) 141 | end) 142 | end 143 | 144 | g.test_graphite_sends_data_to_socket = function(cg) 145 | local port = 22003 146 | local sock = socket('AF_INET', 'SOCK_DGRAM', 'udp') 147 | sock:bind('127.0.0.1', port) 148 | 149 | cg.server:exec(function(port) -- luacheck: ignore 431 150 | local metrics = require('metrics') 151 | local graphite = require('metrics.plugins.graphite') 152 | 153 | local cnt = metrics.counter('test_cnt', 'test-cnt') 154 | cnt:inc(1) 155 | graphite.init({port = port}) 156 | end, {port}) 157 | 158 | require('fiber').sleep(0.5) 159 | local graphite_obs = sock:recvfrom(50) 160 | local obs_table = graphite_obs:split(' ') 161 | t.assert_equals(obs_table[1], 'tarantool.test_cnt') 162 | t.assert_equals(obs_table[2], '1') 163 | sock:close() 164 | end 165 | 166 | g.test_graphite_kills_previous_fibers_on_init = function(cg) 167 | cg.server:exec(function() 168 | local fiber = require('fiber') 169 | local fun = require('fun') 170 | local graphite = require('metrics.plugins.graphite') 171 | 172 | local function mock_graphite_worker() 173 | fiber.create(function() 174 | fiber.name('metrics_graphite_worker') 175 | fiber.sleep(math.huge) 176 | end) 177 | end 178 | 179 | local function count_workers() 180 | return fun.iter(fiber.info()): 181 | filter(function(_, x) return x.name == 'metrics_graphite_worker' end): 182 | length() 183 | end 184 | 185 | t.assert_equals(count_workers(), 0) 186 | mock_graphite_worker() 187 | mock_graphite_worker() 188 | t.assert_equals(count_workers(), 2) 189 | 190 | graphite.init({}) 191 | fiber.yield() -- let cancelled fibers disappear from fiber.info() 192 | t.assert_equals(count_workers(), 1) 193 | end) 194 | end 195 | -------------------------------------------------------------------------------- /test/plugins/json_test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local t = require('luatest') 4 | local g = t.group('json_plugin') 5 | 6 | local json_exporter = require('metrics.plugins.json') 7 | local metrics = require('metrics') 8 | local json = require('json') 9 | local utils = require('test.utils') 10 | 11 | g.after_each(function() 12 | -- Delete all collectors and global labels 13 | metrics.clear() 14 | end) 15 | 16 | g.test_infinite_value_serialization = function() 17 | local test_nan = metrics.gauge('test_nan') 18 | local test_inf = metrics.gauge('test_inf') 19 | 20 | test_nan:set( metrics.NAN, { type = metrics.NAN } ) 21 | test_inf:set( metrics.INF, { type = metrics.INF } ) 22 | test_inf:set(-metrics.INF, { type = -metrics.INF } ) 23 | 24 | local json_metrics = json.decode(json_exporter.export()) 25 | 26 | local nan_obs = utils.find_obs( 27 | 'test_nan', { type = tostring(metrics.NAN) }, json_metrics) 28 | t.assert_equals(nan_obs.value, tostring( metrics.NAN ), 'check NaN') 29 | 30 | local inf_pos_obs = utils.find_obs( 31 | 'test_inf', { type = tostring(metrics.INF) }, json_metrics) 32 | t.assert_equals(inf_pos_obs.value, tostring( metrics.INF ), 'check INF') 33 | 34 | local inf_neg_obs = utils.find_obs( 35 | 'test_inf', { type = tostring(-metrics.INF) }, json_metrics) 36 | t.assert_equals(inf_neg_obs.value, tostring(-metrics.INF ), 'check -INF') 37 | end 38 | 39 | g.test_number_value_serialization = function() 40 | local test_num_float = metrics.gauge('number_float') 41 | local test_num_int = metrics.counter('number_int') 42 | 43 | test_num_float:set(0.333, { type = 'float', nan = metrics.NAN }) 44 | test_num_int:inc(10, { type = 'int', inf = metrics.INF }) 45 | 46 | local json_metrics = json.decode(json_exporter.export()) 47 | 48 | local float_obs = utils.find_obs( 49 | 'number_float', 50 | { type = 'float', nan = tostring(metrics.NAN) }, json_metrics) 51 | 52 | local int_obs = utils.find_obs( 53 | 'number_int', 54 | { type = 'int', inf = tostring(metrics.INF) }, json_metrics) 55 | 56 | t.assert_equals(float_obs.value, 0.333, 'check float') 57 | t.assert_equals(int_obs.value, 10, 'check int') 58 | end 59 | 60 | g.test_number64_ll_value_parses_to_json_number = function() 61 | local gauge_ll = metrics.gauge('test_cdata_ll') 62 | gauge_ll:set(-9007199254740992LL) 63 | 64 | local json_metrics = json.decode(json_exporter.export()) 65 | local obs_ll = utils.find_obs('test_cdata_ll', {}, json_metrics) 66 | t.assert_not_equals(type(obs_ll.value), 'string', 'number64 is not casted to string on export') 67 | t.assert_equals(obs_ll.value, -9007199254740992LL, 'number64 LL parsed to corrent number value') 68 | end 69 | 70 | g.test_number64_ull_value_parses_to_json_number = function() 71 | local gauge_ull = metrics.gauge('test_cdata_ull') 72 | gauge_ull:set(9007199254740992ULL) 73 | 74 | local json_metrics = json.decode(json_exporter.export()) 75 | local obs_ull = utils.find_obs('test_cdata_ull', {}, json_metrics) 76 | t.assert_not_equals(type(obs_ull.value), 'string', 'number64 is not casted to string on export') 77 | t.assert_equals(obs_ull.value, 9007199254740992ULL, 'number64 ULL parsed to corrent number value') 78 | end 79 | -------------------------------------------------------------------------------- /test/plugins/prometheus_test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | require('strict').on() 4 | 5 | local t = require('luatest') 6 | local g = t.group('prometheus_plugin') 7 | 8 | local utils = require('test.utils') 9 | 10 | g.before_all(function(cg) 11 | utils.create_server(cg) 12 | cg.prometheus_metrics = cg.server:exec(function() 13 | local s = box.schema.space.create( 14 | 'random_space_for_prometheus', 15 | {if_not_exists = true}) 16 | s:create_index('pk', {if_not_exists = true}) 17 | 18 | local s_vinyl = box.schema.space.create( 19 | 'random_vinyl_space_for_prometheus', 20 | {if_not_exists = true, engine = 'vinyl'}) 21 | s_vinyl:create_index('pk', {if_not_exists = true}) 22 | 23 | -- Enable default metrics collections 24 | require('metrics').enable_default_metrics() 25 | 26 | return require('metrics.plugins.prometheus').collect_http().body 27 | end) 28 | end) 29 | 30 | g.after_all(utils.drop_server) 31 | 32 | g.test_ll_ull_postfixes = function() 33 | local resp = g.prometheus_metrics 34 | t.assert_not(resp:match("ULL") ~= nil or resp:match("LL") ~= nil, 35 | "Plugin output contains cdata postfixes") 36 | end 37 | 38 | g.test_cdata_handling = function() 39 | t.assert_str_contains(g.prometheus_metrics, 'tnt_space_bsize{name="random_space_for_prometheus",engine="memtx"} 0', 40 | 'Plugin output serialize 0ULL as +Inf') 41 | end 42 | -------------------------------------------------------------------------------- /test/promtool.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | local fio = require('fio') 3 | local prometheus = require('metrics.plugins.prometheus') 4 | local metrics = require('metrics') 5 | 6 | metrics.enable_default_metrics() 7 | 8 | local counter = metrics.counter('counter_total', 'help text') 9 | local gauge = metrics.gauge('gauge', 'help text') 10 | local histogram = metrics.histogram('histogran', 'help text', nil) 11 | local summary = metrics.summary('summary', 'help text', {[0.5] = 0.01, [0.9] = 0.01, [0.99] = 0.01}, 12 | {max_age_time = 60, age_buckets_count = 5}) 13 | for i = 1, 10^3 do 14 | summary:observe(i) 15 | histogram:observe(i) 16 | end 17 | counter:inc(1) 18 | gauge:set(1) 19 | 20 | local fh = fio.open('prometheus-input', {'O_RDWR', 'O_CREAT'}, tonumber('644',8)) 21 | fh:write(prometheus.collect_http().body) 22 | fh:close() 23 | -------------------------------------------------------------------------------- /test/psutils_linux_test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local t = require('luatest') 4 | local g = t.group('psutils_linux') 5 | local utils = require('test.utils') 6 | 7 | g.before_all(function(cg) 8 | t.skip_if(jit.os ~= 'Linux', 'Linux is the only supported platform') 9 | utils.create_server(cg) 10 | 11 | cg.server:exec(function() 12 | local fio = require('fio') 13 | local fio_impl = table.deepcopy(fio) 14 | 15 | local psutils_mock = require('test.psutils_linux_test_payload') 16 | 17 | fio.open = function(path, mode) 18 | return fio_impl.open(psutils_mock.files[path], mode) 19 | end 20 | fio.listdir = function(_) 21 | return fio_impl.listdir(psutils_mock.task_dir_path) 22 | end 23 | end) 24 | end) 25 | 26 | g.after_all(utils.drop_server) 27 | 28 | g.test_get_cpu_time = function(cg) 29 | cg.server:exec(function() 30 | local psutils_linux = require('metrics.psutils.psutils_linux') 31 | local expected = 138445 32 | t.assert_equals(psutils_linux.get_cpu_time(), expected) 33 | end) 34 | end 35 | 36 | g.test_get_process_cpu_time = function(cg) 37 | cg.server:exec(function() 38 | local psutils_linux = require('metrics.psutils.psutils_linux') 39 | local expected = { 40 | {pid = 1, comm = 'tarantool', utime = 468, stime = 171}, 41 | {pid = 12, comm = 'coio', utime = 0, stime = 0}, 42 | {pid = 13, comm = 'iproto', utime = 118, stime = 534}, 43 | {pid = 14, comm = 'wal', utime = 0, stime = 0}, 44 | } 45 | t.assert_items_equals(psutils_linux.get_process_cpu_time(), expected) 46 | end) 47 | end 48 | 49 | g.test_get_cpu_count = function(cg) 50 | cg.server:exec(function() 51 | local psutils_linux = require('metrics.psutils.psutils_linux') 52 | t.assert_gt(psutils_linux.get_cpu_count(), 0) 53 | end) 54 | end 55 | -------------------------------------------------------------------------------- /test/psutils_linux_test_payload/init.lua: -------------------------------------------------------------------------------- 1 | local payload_dir = debug.sourcedir() 2 | local stat_file_path = payload_dir .. '/proc_stat' 3 | local task_dir_path = payload_dir .. '/proc_self_task' 4 | 5 | return { 6 | files = { 7 | ['/proc/stat'] = stat_file_path, 8 | ['/proc/self/task/1/stat'] = task_dir_path .. '/1/stat', 9 | ['/proc/self/task/12/stat'] = task_dir_path .. '/12/stat', 10 | ['/proc/self/task/13/stat'] = task_dir_path .. '/13/stat', 11 | ['/proc/self/task/14/stat'] = task_dir_path .. '/14/stat', 12 | }, 13 | task_dir_path = task_dir_path, 14 | } 15 | -------------------------------------------------------------------------------- /test/psutils_linux_test_payload/proc_self_task/1/stat: -------------------------------------------------------------------------------- 1 | 1 (tarantool) S 0 1 1 0 -1 4210944 16715 352 16 1 468 171 0 0 20 0 4 0 44707 867078144 12731 18446744073709551615 1 1 0 0 0 0 0 4096 134301315 0 0 0 17 1 0 0 5 0 0 0 0 0 0 0 0 0 0 2 | -------------------------------------------------------------------------------- /test/psutils_linux_test_payload/proc_self_task/12/stat: -------------------------------------------------------------------------------- 1 | 12 (coio) S 0 1 1 0 -1 1077952832 2 352 0 1 0 0 0 0 20 0 4 0 44749 867078144 12805 18446744073709551615 1 1 0 0 0 0 2147221247 4096 134301315 0 0 0 -1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 2 | -------------------------------------------------------------------------------- /test/psutils_linux_test_payload/proc_self_task/13/stat: -------------------------------------------------------------------------------- 1 | 13 (iproto) S 0 1 1 0 -1 4210752 38 352 0 1 118 534 0 0 20 0 4 0 44750 867078144 12844 18446744073709551615 1 1 0 0 0 0 2147221247 4096 134301315 0 0 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 | -------------------------------------------------------------------------------- /test/psutils_linux_test_payload/proc_self_task/14/stat: -------------------------------------------------------------------------------- 1 | 14 (wal) S 0 1 1 0 -1 4210752 13 352 0 1 0 0 0 0 20 0 4 0 44750 867078144 12915 18446744073709551615 1 1 0 0 0 0 2147221247 4096 134301315 0 0 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 | -------------------------------------------------------------------------------- /test/psutils_linux_test_payload/proc_stat: -------------------------------------------------------------------------------- 1 | cpu 7926 0 3217 125444 817 0 1041 0 0 0 2 | cpu0 3531 0 1455 63037 335 0 581 0 0 0 3 | cpu1 4394 0 1762 62406 482 0 460 0 0 0 4 | intr 912515 129 0 0 86 393 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 34 0 32344 2680 48 1531 0 106484 1 0 27948 27863 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 | ctxt 1720741 6 | btime 1591012657 7 | processes 3386 8 | procs_running 3 9 | procs_blocked 0 10 | softirq 830240 0 178712 3648 465901 4144 0 5 100863 0 76967 11 | -------------------------------------------------------------------------------- /test/psutils_linux_thread_clean_test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local t = require('luatest') 4 | local g = t.group('psutils_linux_clean_info') 5 | local utils = require('test.utils') 6 | 7 | g.before_all(function(cg) 8 | t.skip_if(jit.os ~= 'Linux', 'Linux is the only supported platform') 9 | utils.create_server(cg) 10 | end) 11 | 12 | g.after_all(utils.drop_server) 13 | 14 | g.after_each(function(cg) 15 | cg.server:exec(function() 16 | require('metrics').clear() 17 | end) 18 | end) 19 | 20 | g.test_clean_thread_info = function(cg) 21 | cg.server:exec(function() 22 | local metrics = require('metrics') 23 | local utils = require('test.utils') -- luacheck: ignore 431 24 | 25 | require('metrics.psutils.cpu').update() 26 | 27 | local observations = metrics.collect() 28 | 29 | local thread_obs = utils.find_metric('tnt_cpu_thread', observations) 30 | t.assert_not_equals(thread_obs, nil) 31 | 32 | local threads = {} 33 | for _, obs in ipairs(thread_obs) do 34 | threads[obs.label_pairs.thread_pid] = true 35 | end 36 | 37 | -- After box.cfg{}, there should be at least tx (tarantool), iproto and wal. 38 | t.assert_ge(utils.len(threads), 3) 39 | t.assert_equals(#thread_obs, 2 * utils.len(threads), 40 | 'There are two observations for each thread (user and system)') 41 | end) 42 | end 43 | 44 | g.test_cpu_count = function(cg) 45 | cg.server:exec(function() 46 | local metrics = require('metrics') 47 | local utils = require('test.utils') -- luacheck: ignore 431 48 | 49 | require('metrics.psutils.cpu').update() 50 | 51 | local observations = metrics.collect() 52 | local metric = utils.find_metric('tnt_cpu_number', observations) 53 | t.assert_not_equals(metric, nil) 54 | t.assert_equals(#metric, 1) 55 | t.assert_gt(metric[1].value, 0) 56 | end) 57 | end 58 | 59 | 60 | g.test_clear = function(cg) 61 | cg.server:exec(function() 62 | local metrics = require('metrics') 63 | local utils = require('test.utils') -- luacheck: ignore 431 64 | local cpu = require('metrics.psutils.cpu') 65 | 66 | local function check_cpu_metrics(presents) 67 | 68 | local observations = metrics.collect() 69 | 70 | local expected = { 71 | 'tnt_cpu_number', 72 | 'tnt_cpu_time', 73 | 'tnt_cpu_thread', 74 | } 75 | 76 | for _, metric_name in ipairs(expected) do 77 | local metric = utils.find_metric(metric_name, observations) 78 | 79 | if presents then 80 | t.assert_not_equals(metric, nil) 81 | else 82 | t.assert_equals(metric, nil) 83 | end 84 | end 85 | end 86 | 87 | cpu.update() 88 | check_cpu_metrics(true) 89 | 90 | cpu.clear() 91 | check_cpu_metrics(false) 92 | 93 | cpu.update() 94 | check_cpu_metrics(true) 95 | end) 96 | end 97 | 98 | g.test_default_metrics_metainfo = function(cg) 99 | cg.server:exec(function() 100 | require('metrics.psutils.cpu').update() 101 | 102 | for k, c in pairs(require('metrics').collectors()) do 103 | t.assert_equals(c.metainfo.default, true, 104 | ('psutils collector %s has metainfo label "default"'):format(k)) 105 | end 106 | end) 107 | end 108 | -------------------------------------------------------------------------------- /test/quantile_test.lua: -------------------------------------------------------------------------------- 1 | local quantile = require('metrics.quantile') 2 | local fiber = require('fiber') 3 | local ffi = require('ffi') 4 | local t = require('luatest') 5 | local g = t.group('quantile') 6 | 7 | local q = quantile.NewTargeted({[0.5]=0.01, [0.9]=0.01, [0.99]=0.01}) 8 | 9 | -- Tests from 10 | -- https://github.com/beorn7/perks/blob/37c8de3658fcb183f997c4e13e8337516ab753e6/quantile/stream_test.go#L98 11 | 12 | local function getPerc(x, p, eps) 13 | local low = math.ceil(#x * (p - eps)) + 1 14 | if low < 1 then 15 | low = 1 16 | end 17 | local upper = math.ceil(#x * (p + eps)) + 1 18 | if upper > #x then 19 | upper = #x 20 | end 21 | 22 | return x[low], x[upper] 23 | end 24 | 25 | local x = {} 26 | math.randomseed(0) 27 | for i = 1, 10^4 + 100 do 28 | local m = math.random() * 10^6 29 | -- Add 5% asymmetric outliers. 30 | if i % 20 == 0 then 31 | m = m^2 + 1 32 | end 33 | table.insert(x, m) 34 | quantile.Insert(q, m) 35 | end 36 | 37 | table.sort(x) 38 | 39 | local function assert_quantile(quan) 40 | local wlow, wupper = getPerc(x, quan, 0.01) 41 | local d = quantile.Query(q, quan) 42 | t.assert_le(wlow, d) 43 | t.assert_le(d, wupper) 44 | end 45 | 46 | g.test_query_05 = function() 47 | assert_quantile(0.5) 48 | end 49 | 50 | g.test_query_09 = function() 51 | assert_quantile(0.9) 52 | end 53 | 54 | g.test_query_099 = function() 55 | assert_quantile(0.99) 56 | end 57 | 58 | g.test_wrong_quantiles = function() 59 | t.assert_error_msg_contains( 60 | 'Quantile must be in [0; 1]', 61 | quantile.NewTargeted, 62 | {0.5, 0.9, 0.99} 63 | ) 64 | end 65 | 66 | g.test_wrong_max_samples = function() 67 | t.assert_error_msg_contains( 68 | 'max_samples must be positive', 69 | quantile.NewTargeted, 70 | {[0.5]=0.01, [0.9]=0.01, [0.99]=0.01}, 71 | 0 72 | ) 73 | end 74 | 75 | local ARR_SIZE = 500 76 | 77 | local function assert_sorted(arr, low, high) 78 | for i = low + 1, high do 79 | t.assert(arr[i] >= arr[i-1]) 80 | end 81 | end 82 | 83 | g.test_low_values_sorted = function() 84 | local lows = ffi.new('double[?]', ARR_SIZE) 85 | for i = 0, ARR_SIZE - 1 do 86 | lows[i] = math.random()*10^-6 87 | end 88 | quantile.quicksort(lows, 0, ARR_SIZE - 1) 89 | assert_sorted(lows, 0, ARR_SIZE - 1) 90 | end 91 | 92 | g.test_random_values_sorted = function() 93 | local rands = ffi.new('double[?]', ARR_SIZE) 94 | for i = 0, ARR_SIZE - 1 do 95 | rands[i] = math.random()*2*10^3 - 10^3 96 | end 97 | quantile.quicksort(rands, 0, ARR_SIZE - 1) 98 | assert_sorted(rands, 0, ARR_SIZE - 1) 99 | end 100 | 101 | g.test_low_bound_negative = function() 102 | local empty = ffi.new('double[?]', 2) 103 | t.assert_error_msg_contains( 104 | 'Low bound must be non-negative', 105 | quantile.quicksort, 106 | empty, 107 | -1, 108 | 1 109 | ) 110 | end 111 | 112 | g.test_high_bound_higher_array_size = function() 113 | local empty = ffi.new('double[?]', 2) 114 | t.assert_error_msg_contains( 115 | 'Upper bound must be lower than array size', 116 | quantile.quicksort, 117 | empty, 118 | 1, 119 | 10 120 | ) 121 | end 122 | 123 | g.test_not_sorted = function() 124 | local array = ffi.new('double[?]', 2) 125 | array[0] = math.huge 126 | array[1] = -math.huge 127 | quantile.quicksort(array, 0, 0) 128 | t.assert_not(array[1] >= array[0]) 129 | end 130 | 131 | g.test_package_reload = function() 132 | package.loaded['metrics.quantile'] = nil 133 | local ok, quantile_package = pcall(require, 'metrics.quantile') 134 | t.assert(ok, quantile_package) 135 | end 136 | 137 | g.test_fiber_yield = function() 138 | local q1 = quantile.NewTargeted({[0.5]=0.01, [0.9]=0.01, [0.99]=0.01}, 1000) 139 | 140 | for _=1,10 do 141 | fiber.create(function() 142 | for _=1,1e2 do 143 | t.assert(q1.b_len < q1.__max_samples) 144 | quantile.Insert(q1, math.random(1000)) 145 | end 146 | end) 147 | end 148 | 149 | for _=1,10 do 150 | t.assert(q1.b_len < q1.__max_samples) 151 | quantile.Insert(q1, math.random(1)) 152 | end 153 | end 154 | 155 | g.test_query_on_empty_quantile = function() 156 | local emptyQuantile = quantile.NewTargeted({[0.5]=0.01, [0.9]=0.01, [0.99]=0.01}) 157 | 158 | local res = quantile.Query(emptyQuantile, 0.99) 159 | 160 | t.assert_nan(res) 161 | end 162 | 163 | g.test_reset = function() 164 | local Quantile = quantile.NewTargeted({[0.5]=0.01, [0.9]=0.01, [0.99]=0.01}, 10) 165 | for _ = 1,20 do 166 | quantile.Insert(Quantile, math.random()) 167 | end 168 | 169 | local res = quantile.Query(Quantile, 0.99) 170 | t.assert_not_nan(res) 171 | 172 | quantile.Reset(Quantile) 173 | 174 | res = quantile.Query(Quantile, 0.99) 175 | t.assert_nan(res) 176 | end 177 | 178 | g.test_quantile_insert_works_after_reset = function() 179 | local Quantile = quantile.NewTargeted({[0.5]=0.01, [0.9]=0.01, [0.99]=0.01}, 10) 180 | 181 | quantile.Insert(Quantile, math.random()) 182 | quantile.Reset(Quantile) 183 | quantile.Insert(Quantile, math.random()) 184 | 185 | local res = quantile.Query(Quantile, 0.5) 186 | t.assert_not_equals(res, math.huge) 187 | end 188 | 189 | g.test_quantile_values_present_after_buffer_flush = function() 190 | local Quantile = quantile.NewTargeted({[0.5]=0.01, [0.9]=0.01, [0.99]=0.01}, 10) 191 | 192 | for _ = 1, 10 do 193 | quantile.Insert(Quantile, math.random()) 194 | end 195 | 196 | t.assert(Quantile:flushed()) 197 | -- buffer now is flushed 198 | 199 | local res = quantile.Query(Quantile, 0.5) 200 | t.assert_not_nan(res) 201 | end 202 | -------------------------------------------------------------------------------- /test/rock_utils.lua: -------------------------------------------------------------------------------- 1 | local loaders_ok, loaders = pcall(require, 'internal.loaders') 2 | 3 | local function traverse_rock(func, name) 4 | func(name, package.loaded[name]) 5 | 6 | for subpkg_name, subpkg in pairs(package.loaded) do 7 | if subpkg_name:startswith(name .. '.') then 8 | func(subpkg_name, subpkg) 9 | end 10 | end 11 | end 12 | 13 | -- Package may have table cross-references. 14 | local MAX_DEPTH = 8 15 | 16 | -- Package functions contain useful debug info. 17 | local function traverse_pkg_func(func, name, pkg, max_depth) 18 | max_depth = max_depth or MAX_DEPTH 19 | if max_depth <= 0 then 20 | return 21 | end 22 | 23 | if type(pkg) == 'function' then 24 | func(name, pkg) 25 | return 26 | end 27 | 28 | if type(pkg) ~= 'table' then 29 | return 30 | end 31 | 32 | for _, v in pairs(pkg) do 33 | traverse_pkg_func(func, name, v, max_depth - 1) 34 | end 35 | 36 | local mt = getmetatable(pkg) 37 | if mt ~= nil and mt.__call ~= nil then 38 | func(name, mt.__call) 39 | end 40 | end 41 | 42 | local function remove_loaded_pkg(name, _) 43 | package.loaded[name] = nil 44 | end 45 | 46 | local function remove_builtin_pkg(name, _) 47 | remove_loaded_pkg(name) 48 | if loaders_ok then 49 | loaders.builtin[name] = nil 50 | end 51 | end 52 | 53 | local function assert_nonbuiltin_func(pkg_name, func) 54 | local source = debug.getinfo(func).source 55 | assert(source:match('^@builtin') == nil, 56 | ("package %s is built-in, cleanup failed"):format(pkg_name)) 57 | end 58 | 59 | local function assert_builtin_func(pkg_name, func) 60 | local source = debug.getinfo(func).source 61 | assert(source:match('^@builtin') ~= nil, 62 | ("package %s is external, built-in expected"):format(pkg_name)) 63 | end 64 | 65 | local function assert_nonbuiltin_pkg(name, pkg) 66 | traverse_pkg_func(assert_nonbuiltin_func, name, pkg) 67 | end 68 | 69 | local function assert_builtin_pkg(name, pkg) 70 | traverse_pkg_func(assert_builtin_func, name, pkg) 71 | end 72 | 73 | local function remove_builtin_rock(name) 74 | traverse_rock(remove_builtin_pkg, name) 75 | end 76 | 77 | local function remove_loaded_rock(name) 78 | traverse_rock(remove_loaded_pkg, name) 79 | end 80 | 81 | local function remove_override_rock(name) 82 | loaders.override_builtin_disable() 83 | traverse_rock(remove_loaded_pkg, name) 84 | end 85 | 86 | local function assert_nonbuiltin_rock(name) 87 | require(name) 88 | traverse_rock(assert_nonbuiltin_pkg, name) 89 | end 90 | 91 | local function assert_builtin_rock(name) 92 | require(name) 93 | traverse_rock(assert_builtin_pkg, name) 94 | end 95 | 96 | return { 97 | assert_builtin = assert_builtin_rock, 98 | assert_nonbuiltin = assert_nonbuiltin_rock, 99 | remove_builtin = remove_builtin_rock, 100 | remove_loaded = remove_loaded_rock, 101 | remove_override = remove_override_rock, 102 | } 103 | -------------------------------------------------------------------------------- /test/tarantool/cpu_metrics_test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local t = require('luatest') 4 | local g = t.group('cpu_metric') 5 | local utils = require('test.utils') 6 | 7 | g.before_all(utils.create_server) 8 | 9 | g.after_all(utils.drop_server) 10 | 11 | g.before_each(function(cg) 12 | cg.server:exec(function() require('metrics').clear() end) 13 | end) 14 | 15 | g.test_cpu = function(cg) 16 | cg.server:exec(function() 17 | t.skip('Flaky test, see https://github.com/tarantool/metrics/issues/492') 18 | 19 | local metrics = require('metrics') 20 | local cpu = require('metrics.tarantool.cpu') 21 | local utils = require('test.utils') -- luacheck: ignore 431 22 | 23 | metrics.enable_default_metrics() 24 | cpu.update() 25 | local default_metrics = metrics.collect() 26 | local user_time_metric = utils.find_metric('tnt_cpu_user_time', default_metrics) 27 | local system_time_metric = utils.find_metric('tnt_cpu_system_time', default_metrics) 28 | t.assert(user_time_metric) 29 | t.assert(system_time_metric) 30 | t.assert(user_time_metric[1].value > 0) 31 | t.assert(system_time_metric[1].value > 0) 32 | end) 33 | end 34 | -------------------------------------------------------------------------------- /test/tarantool/info_metrics_test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local t = require('luatest') 4 | local g = t.group('info_metric') 5 | local utils = require('test.utils') 6 | 7 | g.before_all(utils.create_server) 8 | 9 | g.after_all(utils.drop_server) 10 | 11 | g.before_each(function(cg) 12 | cg.server:exec(function() require('metrics').clear() end) 13 | end) 14 | 15 | g.test_info = function(cg) 16 | cg.server:exec(function() 17 | local metrics = require('metrics') 18 | local info = require('metrics.tarantool.info') 19 | local utils = require('test.utils') -- luacheck: ignore 431 20 | 21 | metrics.enable_default_metrics() 22 | info.update() 23 | local default_metrics = metrics.collect() 24 | 25 | local tnt_info_lsn = utils.find_metric('tnt_info_lsn', default_metrics) 26 | t.assert(tnt_info_lsn) 27 | t.assert_type(tnt_info_lsn[1].value, 'number') 28 | 29 | local tnt_info_uptime = utils.find_metric('tnt_info_uptime', default_metrics) 30 | t.assert(tnt_info_uptime) 31 | t.assert_type(tnt_info_uptime[1].value, 'number') 32 | end) 33 | end 34 | 35 | g.test_synchro_queue = function(cg) 36 | t.skip_if(utils.is_version_less(_TARANTOOL, '2.7.0'), 37 | 'Tarantool version is must be v2.7.0 or greater') 38 | 39 | cg.server:exec(function() 40 | local metrics = require('metrics') 41 | local info = require('metrics.tarantool.info') 42 | local utils = require('test.utils') -- luacheck: ignore 431 43 | 44 | metrics.enable_default_metrics() 45 | info.update() 46 | local default_metrics = metrics.collect() 47 | 48 | local tnt_synchro_queue_owner = utils.find_metric('tnt_synchro_queue_owner', default_metrics) 49 | t.assert(tnt_synchro_queue_owner) 50 | t.assert_type(tnt_synchro_queue_owner[1].value, 'number') 51 | 52 | local tnt_synchro_queue_term = utils.find_metric('tnt_synchro_queue_term', default_metrics) 53 | t.assert(tnt_synchro_queue_term) 54 | t.assert_type(tnt_synchro_queue_term[1].value, 'number') 55 | 56 | local tnt_synchro_queue_len = utils.find_metric('tnt_synchro_queue_len', default_metrics) 57 | t.assert(tnt_synchro_queue_len) 58 | t.assert_type(tnt_synchro_queue_len[1].value, 'number') 59 | 60 | local tnt_synchro_queue_busy = utils.find_metric('tnt_synchro_queue_busy', default_metrics) 61 | t.assert(tnt_synchro_queue_busy) 62 | t.assert_type(tnt_synchro_queue_busy[1].value, 'number') 63 | end) 64 | end 65 | 66 | g.test_election = function(cg) 67 | t.skip_if(utils.is_version_less(_TARANTOOL, '2.7.0'), 68 | 'Tarantool version is must be v2.7.0 or greater') 69 | 70 | cg.server:exec(function() 71 | local metrics = require('metrics') 72 | local info = require('metrics.tarantool.info') 73 | local utils = require('test.utils') -- luacheck: ignore 431 74 | 75 | metrics.enable_default_metrics() 76 | info.update() 77 | local default_metrics = metrics.collect() 78 | 79 | local tnt_election_state = utils.find_metric('tnt_election_state', default_metrics) 80 | t.assert(tnt_election_state) 81 | t.assert_type(tnt_election_state[1].value, 'number') 82 | 83 | local tnt_election_vote = utils.find_metric('tnt_election_vote', default_metrics) 84 | t.assert(tnt_election_vote) 85 | t.assert_type(tnt_election_vote[1].value, 'number') 86 | 87 | local tnt_election_leader = utils.find_metric('tnt_election_leader', default_metrics) 88 | t.assert(tnt_election_leader) 89 | t.assert_type(tnt_election_leader[1].value, 'number') 90 | 91 | local tnt_election_term = utils.find_metric('tnt_election_term', default_metrics) 92 | t.assert(tnt_election_term) 93 | t.assert_type(tnt_election_term[1].value, 'number') 94 | end) 95 | end 96 | 97 | g.test_election_leader_idle = function(cg) 98 | t.skip_if(utils.is_version_less(_TARANTOOL, '2.10.0'), 99 | 'Tarantool version is must be v2.10.0 or greater') 100 | 101 | cg.server:exec(function() 102 | box.cfg{election_mode = 'candidate'} 103 | local metrics = require('metrics') 104 | local info = require('metrics.tarantool.info') 105 | local utils = require('test.utils') -- luacheck: ignore 431 106 | 107 | metrics.enable_default_metrics() 108 | info.update() 109 | local default_metrics = metrics.collect() 110 | 111 | local tnt_election_leader_idle = utils.find_metric( 112 | 'tnt_election_leader_idle', default_metrics 113 | ) 114 | t.assert(tnt_election_leader_idle) 115 | t.assert_type(tnt_election_leader_idle[1].value, 'number') 116 | box.cfg{election_mode = 'off'} 117 | end) 118 | end 119 | -------------------------------------------------------------------------------- /test/tarantool/lj_metrics_test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | require('strict').on() 4 | 5 | local t = require('luatest') 6 | local g = t.group('luajit_metrics') 7 | local utils = require('test.utils') 8 | 9 | g.before_all(function(cg) 10 | t.skip_if(jit.os ~= 'Linux', 'Linux is the only supported platform') 11 | utils.create_server(cg) 12 | end) 13 | 14 | g.after_all(utils.drop_server) 15 | 16 | g.after_each(function(cg) 17 | cg.server:exec(function() 18 | require('metrics').clear() 19 | end) 20 | end) 21 | 22 | g.test_lj_metrics = function(cg) 23 | cg.server:exec(function() 24 | local metrics = require('metrics') 25 | 26 | local metrics_available = pcall(require, 'misc') 27 | t.skip_if(metrics_available == false, 'metrics are not available') 28 | 29 | metrics.enable_default_metrics() 30 | 31 | local lj_metrics = {} 32 | for _, v in pairs(metrics.collect{invoke_callbacks = true}) do 33 | if v.metric_name:startswith('lj_') then 34 | table.insert(lj_metrics, v.metric_name) 35 | end 36 | end 37 | table.sort(lj_metrics) 38 | 39 | local expected_lj_metrics = { 40 | "lj_gc_freed_total", 41 | "lj_strhash_hit_total", 42 | "lj_gc_steps_atomic_total", 43 | "lj_strhash_miss_total", 44 | "lj_gc_steps_sweepstring_total", 45 | "lj_gc_strnum", 46 | "lj_gc_tabnum", 47 | "lj_gc_cdatanum", 48 | "lj_jit_snap_restore_total", 49 | "lj_gc_memory", 50 | "lj_gc_udatanum", 51 | "lj_gc_steps_finalize_total", 52 | "lj_gc_allocated_total", 53 | "lj_jit_trace_num", 54 | "lj_gc_steps_sweep_total", 55 | "lj_jit_trace_abort_total", 56 | "lj_jit_mcode_size", 57 | "lj_gc_steps_propagate_total", 58 | "lj_gc_steps_pause_total", 59 | } 60 | table.sort(expected_lj_metrics) 61 | 62 | t.assert_equals(#lj_metrics, #expected_lj_metrics) 63 | t.assert_items_equals(lj_metrics, expected_lj_metrics) 64 | end) 65 | end 66 | -------------------------------------------------------------------------------- /test/tarantool/memtx_metrics_test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local t = require('luatest') 4 | local g = t.group('memtx_metric') 5 | local utils = require('test.utils') 6 | 7 | g.before_all(utils.create_server) 8 | 9 | g.after_all(utils.drop_server) 10 | 11 | g.before_each(function(cg) 12 | cg.server:exec(function() require('metrics').clear() end) 13 | end) 14 | 15 | g.test_memtx = function(cg) 16 | t.skip_if(utils.is_version_less(_TARANTOOL, '2.10.0'), 17 | 'Tarantool version is must be v2.10.0 or greater') 18 | 19 | cg.server:exec(function() 20 | local metrics = require('metrics') 21 | local memtx = require('metrics.tarantool.memtx') 22 | local utils = require('test.utils') -- luacheck: ignore 431 23 | 24 | metrics.enable_default_metrics() 25 | memtx.update() 26 | local default_metrics = metrics.collect() 27 | local log = require('log') 28 | 29 | local metrics_list1 = { 30 | 'tnt_memtx_tnx_statements', 31 | 'tnt_memtx_tnx_user', 32 | 'tnt_memtx_tnx_system', 33 | 'tnt_memtx_mvcc_trackers', 34 | 'tnt_memtx_mvcc_conflicts', 35 | } 36 | 37 | for _, item in ipairs(metrics_list1) do 38 | log.info('checking metric: ' .. item) 39 | local metric = utils.find_metric(item, default_metrics) 40 | t.assert(metric) 41 | t.assert_type(metric[1].value, 'number') 42 | t.assert_items_equals(metric[1].label_pairs, {kind = "average"}) 43 | t.assert_items_equals(metric[2].label_pairs, {kind = "total"}) 44 | t.assert_items_equals(metric[3].label_pairs, {kind = "max"}) 45 | end 46 | 47 | local metrics_list2 = { 48 | 'tnt_memtx_mvcc_tuples_tracking_stories', 49 | 'tnt_memtx_mvcc_tuples_tracking_retained', 50 | 'tnt_memtx_mvcc_tuples_used_stories', 51 | 'tnt_memtx_mvcc_tuples_used_retained', 52 | 'tnt_memtx_mvcc_tuples_read_view_stories', 53 | 'tnt_memtx_mvcc_tuples_read_view_retained', 54 | } 55 | 56 | for _, item in ipairs(metrics_list2) do 57 | log.info('checking metric: ' .. item) 58 | local metric = utils.find_metric(item, default_metrics) 59 | t.assert(metric) 60 | t.assert_type(metric[1].value, 'number') 61 | t.assert_items_equals(metric[1].label_pairs, {kind = "total"}) 62 | t.assert_items_equals(metric[2].label_pairs, {kind = "count"}) 63 | end 64 | end) 65 | end 66 | 67 | g.test_memtx_read_view = function(cg) 68 | t.skip_if(utils.is_version_less(_TARANTOOL, '3.1.0'), 69 | 'Tarantool version is must be v3.1.0 or greater') 70 | 71 | cg.server:exec(function() 72 | local metrics = require('metrics') 73 | local memtx = require('metrics.tarantool.memtx') 74 | local utils = require('test.utils') -- luacheck: ignore 431 75 | 76 | metrics.enable_default_metrics() 77 | memtx.update() 78 | local default_metrics = metrics.collect() 79 | local log = require('log') 80 | 81 | local metrics_list = { 82 | 'tnt_memtx_tuples_data_total', 83 | 'tnt_memtx_tuples_data_read_view', 84 | 'tnt_memtx_tuples_data_garbage', 85 | 'tnt_memtx_index_total', 86 | 'tnt_memtx_index_read_view', 87 | } 88 | 89 | for _, item in ipairs(metrics_list) do 90 | log.info('checking metric: ' .. item) 91 | local metric = utils.find_metric(item, default_metrics) 92 | t.assert(metric) 93 | t.assert_type(metric[1].value, 'number') 94 | end 95 | end) 96 | end 97 | -------------------------------------------------------------------------------- /test/tarantool/vinyl_test.lua: -------------------------------------------------------------------------------- 1 | require('strict').on() 2 | 3 | local t = require('luatest') 4 | local g = t.group() 5 | 6 | local utils = require('test.utils') 7 | 8 | g.before_all(utils.create_server) 9 | 10 | g.after_all(utils.drop_server) 11 | 12 | g.before_each(function(cg) 13 | cg.server:exec(function() 14 | local s_vinyl = box.schema.space.create( 15 | 'test_space', 16 | {if_not_exists = true, engine = 'vinyl'}) 17 | s_vinyl:create_index('pk', {if_not_exists = true}) 18 | require('metrics').enable_default_metrics() 19 | end) 20 | end) 21 | 22 | g.test_vinyl_metrics_present = function(cg) 23 | cg.server:exec(function() 24 | local metrics = require('metrics') 25 | local fun = require('fun') 26 | local utils = require('test.utils') -- luacheck: ignore 431 27 | 28 | local metrics_cnt = fun.iter(metrics.collect{invoke_callbacks = true}):filter(function(x) 29 | return x.metric_name:find('tnt_vinyl') 30 | end):length() 31 | if utils.is_version_less(_TARANTOOL, '2.8.3') 32 | and utils.is_version_greater(_TARANTOOL, '2.0.0') then 33 | t.assert_equals(metrics_cnt, 19) 34 | elseif utils.is_version_less(_TARANTOOL, '3.0.0') then 35 | t.assert_equals(metrics_cnt, 20) 36 | else 37 | t.assert_equals(metrics_cnt, 21) 38 | end 39 | end) 40 | end 41 | -------------------------------------------------------------------------------- /test/tarantool3_helpers/treegen.lua: -------------------------------------------------------------------------------- 1 | -- Borrowed from https://github.com/tarantool/tarantool/blob/b5864c40a0bfc8f26cc65189f3a5c76e441a9396/test/treegen.lua 2 | 3 | -- Working tree generator. 4 | -- 5 | -- Generates a tree of Lua files using provided templates and 6 | -- filenames. Reworked to be used inside the Cluster. 7 | 8 | local fio = require('fio') 9 | local log = require('log') 10 | local fun = require('fun') 11 | 12 | local treegen = {} 13 | 14 | local function find_template(storage, script) 15 | for _, template_def in ipairs(storage.templates) do 16 | if script:match(template_def.pattern) then 17 | return template_def.template 18 | end 19 | end 20 | error(("treegen: can't find a template for script %q"):format(script)) 21 | end 22 | 23 | -- Write provided script into the given directory. 24 | function treegen.write_script(dir, script, body) 25 | local script_abspath = fio.pathjoin(dir, script) 26 | local flags = {'O_CREAT', 'O_WRONLY', 'O_TRUNC'} 27 | local mode = tonumber('644', 8) 28 | 29 | local scriptdir_abspath = fio.dirname(script_abspath) 30 | log.info(('Creating a directory: %s'):format(scriptdir_abspath)) 31 | fio.mktree(scriptdir_abspath) 32 | 33 | log.info(('Writing a script: %s'):format(script_abspath)) 34 | local fh = fio.open(script_abspath, flags, mode) 35 | fh:write(body) 36 | fh:close() 37 | return script_abspath 38 | end 39 | 40 | -- Generate a script that follows a template and write it at the 41 | -- given path in the given directory. 42 | local function gen_script(storage, dir, script, replacements) 43 | local template = find_template(storage, script) 44 | replacements = fun.chain({script = script}, replacements):tomap() 45 | local body = template:gsub('<(.-)>', replacements) 46 | treegen.write_script(dir, script, body) 47 | end 48 | 49 | function treegen.init(storage) 50 | storage.tempdirs = {} 51 | storage.templates = {} 52 | end 53 | 54 | -- Remove all temporary directories created by the test 55 | -- unless KEEP_DATA environment variable is set to a 56 | -- non-empty value. 57 | function treegen.clean(storage) 58 | local dirs = table.copy(storage.tempdirs) or {} 59 | storage.tempdirs = nil 60 | 61 | local keep_data = (os.getenv('KEEP_DATA') or '') ~= '' 62 | 63 | for _, dir in ipairs(dirs) do 64 | if keep_data then 65 | log.info(('Left intact due to KEEP_DATA env var: %s'):format(dir)) 66 | else 67 | log.info(('Recursively removing: %s'):format(dir)) 68 | fio.rmtree(dir) 69 | end 70 | end 71 | 72 | storage.templates = nil 73 | end 74 | 75 | function treegen.add_template(storage, pattern, template) 76 | table.insert(storage.templates, { 77 | pattern = pattern, 78 | template = template, 79 | }) 80 | end 81 | 82 | -- Create a temporary directory with given scripts. 83 | -- 84 | -- The scripts are generated using templates added by 85 | -- treegen.add_template(). 86 | -- 87 | -- Example for {'foo/bar.lua', 'baz.lua'}: 88 | -- 89 | -- / 90 | -- + tmp/ 91 | -- + rfbWOJ/ 92 | -- + foo/ 93 | -- | + bar.lua 94 | -- + baz.lua 95 | -- 96 | -- The return value is '/tmp/rfbWOJ' for this example. 97 | function treegen.prepare_directory(storage, scripts, replacements) 98 | replacements = replacements or {} 99 | 100 | assert(type(scripts) == 'table') 101 | assert(type(replacements) == 'table') 102 | 103 | local dir = fio.tempdir() 104 | 105 | -- fio.tempdir() follows the TMPDIR environment variable. 106 | -- If it ends with a slash, the return value contains a double 107 | -- slash in the middle: for example, if TMPDIR=/tmp/, the 108 | -- result is like `/tmp//rfbWOJ`. 109 | -- 110 | -- It looks harmless on the first glance, but this directory 111 | -- path may be used later to form an URI for a Unix domain 112 | -- socket. As result the URI looks like 113 | -- `unix/:/tmp//rfbWOJ/instance-001.iproto`. 114 | -- 115 | -- It confuses net_box.connect(): it reports EAI_NONAME error 116 | -- from getaddrinfo(). 117 | -- 118 | -- It seems, the reason is a peculiar of the URI parsing: 119 | -- 120 | -- tarantool> uri.parse('unix/:/foo/bar.iproto') 121 | -- --- 122 | -- - host: unix/ 123 | -- service: /foo/bar.iproto 124 | -- unix: /foo/bar.iproto 125 | -- ... 126 | -- 127 | -- tarantool> uri.parse('unix/:/foo//bar.iproto') 128 | -- --- 129 | -- - host: unix 130 | -- path: /foo//bar.iproto 131 | -- ... 132 | -- 133 | -- Let's normalize the path using fio.abspath(), which 134 | -- eliminates the double slashes. 135 | dir = fio.abspath(dir) 136 | 137 | table.insert(storage.tempdirs, dir) 138 | 139 | for _, script in ipairs(scripts) do 140 | gen_script(storage, dir, script, replacements) 141 | end 142 | 143 | return dir 144 | end 145 | 146 | return treegen 147 | -------------------------------------------------------------------------------- /test/unit/cartridge_issues_test.lua: -------------------------------------------------------------------------------- 1 | local helpers = require('test.helper') 2 | 3 | local t = require('luatest') 4 | local g = t.group() 5 | 6 | g.before_all(function() 7 | t.skip_if(type(helpers) ~= 'table', 'Skip cartridge test') 8 | helpers.skip_cartridge_version_less('2.0.2') 9 | end) 10 | 11 | g.test_cartridge_issues_before_cartridge_cfg = function() 12 | require('cartridge.issues') 13 | local issues = require('metrics.cartridge.issues') 14 | local ok, error = pcall(issues.update) 15 | t.assert(ok, error) 16 | end 17 | -------------------------------------------------------------------------------- /test/utils.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | 3 | local fun = require('fun') 4 | local metrics = require('metrics') 5 | 6 | local luatest_utils = require('luatest.utils') 7 | 8 | local utils = {} 9 | 10 | function utils.create_server(g) 11 | g.server = t.Server:new({ 12 | alias = 'myserver', 13 | env = { 14 | LUA_PATH = utils.LUA_PATH 15 | } 16 | }) 17 | g.server:start{wait_until_ready = true} 18 | end 19 | 20 | function utils.drop_server(g) 21 | g.server:drop() 22 | end 23 | 24 | function utils.find_obs(metric_name, label_pairs, observations, comparator) 25 | comparator = comparator or t.assert_equals 26 | 27 | for _, obs in pairs(observations) do 28 | local same_label_pairs = pcall(comparator, obs.label_pairs, label_pairs) 29 | if obs.metric_name == metric_name and same_label_pairs then 30 | return obs 31 | end 32 | end 33 | t.assert_items_include(observations, {metric_name = metric_name, label_pairs = label_pairs}, 34 | 'Missing observation') 35 | end 36 | 37 | function utils.observations_without_timestamps(observations) 38 | return fun.iter(observations or metrics.collect()): 39 | map(function(x) 40 | x.timestamp = nil 41 | return x 42 | end): 43 | totable() 44 | end 45 | 46 | function utils.assert_observations(actual, expected) 47 | t.assert_items_equals( 48 | utils.observations_without_timestamps(actual), 49 | fun.iter(expected):map(function(x) 50 | return { 51 | metric_name = x[1], 52 | value = x[2], 53 | label_pairs = x[3], 54 | } 55 | end):totable() 56 | ) 57 | end 58 | 59 | function utils.find_metric(metric_name, metrics_data) 60 | local m = {} 61 | for _, v in ipairs(metrics_data) do 62 | if v.metric_name == metric_name then 63 | table.insert(m, v) 64 | end 65 | end 66 | return #m > 0 and m or nil 67 | end 68 | 69 | local function to_number_multiple(...) 70 | return unpack(fun.map(tonumber, {...}):totable()) 71 | end 72 | 73 | function utils.is_version_less(ver_str, reference_ver_str) 74 | local major, minor, patch = to_number_multiple(string.match(ver_str, '^(%d+).(%d+).(%d+)')) 75 | local ref_major, ref_minor, ref_patch = to_number_multiple(string.match(reference_ver_str, '^(%d+).(%d+).(%d+)')) 76 | 77 | if ( major < ref_major ) or ( major == ref_major and minor < ref_minor) or 78 | ( major == ref_major and minor == ref_minor and patch < ref_patch) then 79 | return true 80 | else 81 | return false 82 | end 83 | end 84 | 85 | function utils.is_version_greater(ver_str, reference_ver_str) 86 | local major, minor, patch = to_number_multiple(string.match(ver_str, '^(%d+).(%d+).(%d+)')) 87 | local ref_major, ref_minor, ref_patch = to_number_multiple(string.match(reference_ver_str, '^(%d+).(%d+).(%d+)')) 88 | 89 | if ( major > ref_major ) or ( major == ref_major and minor > ref_minor) or 90 | ( major == ref_major and minor == ref_minor and patch > ref_patch) then 91 | return true 92 | else 93 | return false 94 | end 95 | end 96 | 97 | function utils.len(tbl) 98 | local l = 0 99 | for _ in pairs(tbl) do 100 | l = l + 1 101 | end 102 | return l 103 | end 104 | 105 | function utils.clear_spaces() 106 | for _, v in pairs(box.space) do 107 | if v.id > box.schema.SYSTEM_ID_MAX then 108 | v:drop() 109 | end 110 | end 111 | end 112 | 113 | function utils.is_tarantool_3_config_supported() 114 | local tarantool_version = luatest_utils.get_tarantool_version() 115 | return luatest_utils.version_ge(tarantool_version, luatest_utils.version(3, 0, 0)) 116 | end 117 | 118 | -- Empty by default. Empty LUA_PATH satisfies built-in package tests. 119 | -- For tarantool/metrics, LUA_PATH is set up through test.helper 120 | utils.LUA_PATH = nil 121 | 122 | return utils 123 | -------------------------------------------------------------------------------- /test/utils_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local g = t.group() 3 | 4 | local utils = require('metrics.utils') 5 | 6 | g.test_set_gauge = function() 7 | local gauge = utils.set_gauge('gauge', 'gauge info', 10) 8 | 9 | t.assert_equals(gauge.name, 'tnt_gauge') 10 | t.assert_equals(gauge.help, 'gauge info') 11 | t.assert_equals(gauge.observations[''], 10) 12 | end 13 | 14 | g.test_set_counter = function() 15 | local counter = utils.set_counter('counter', 'counter info', 10) 16 | 17 | t.assert_equals(counter.name, 'tnt_counter') 18 | t.assert_equals(counter.help, 'counter info') 19 | t.assert_equals(counter.observations[''], 10) 20 | 21 | utils.set_counter('counter', 'counter info', 20) 22 | t.assert_equals(counter.observations[''], 20) 23 | end 24 | 25 | g.test_set_gauge_prefix = function() 26 | local gauge = utils.set_gauge('gauge', 'gauge info', 10, nil, 'custom_') 27 | 28 | t.assert_equals(gauge.name, 'custom_gauge') 29 | t.assert_equals(gauge.help, 'gauge info') 30 | t.assert_equals(gauge.observations[''], 10) 31 | end 32 | 33 | g.test_set_counter_prefix = function() 34 | local counter = utils.set_counter('counter', 'counter info', 10, nil, 'custom_') 35 | 36 | t.assert_equals(counter.name, 'custom_counter') 37 | t.assert_equals(counter.help, 'counter info') 38 | t.assert_equals(counter.observations[''], 10) 39 | 40 | utils.set_counter('counter', 'counter info', 20, nil, 'custom_') 41 | t.assert_equals(counter.observations[''], 20) 42 | end 43 | 44 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/metrics/7f584b94f59c45d2d83907ab96cc2be7c3b83d16/tmp/.keep --------------------------------------------------------------------------------