Full Code of lomik/graphite-clickhouse for AI

master b816c69a5c9a cached
293 files
1.3 MB
455.3k tokens
1236 symbols
1 requests
Download .txt
Showing preview only (1,442K chars total). Download the full file or copy to clipboard to get everything.
Repository: lomik/graphite-clickhouse
Branch: master
Commit: b816c69a5c9a
Files: 293
Total size: 1.3 MB

Directory structure:
gitextract_n919tixz/

├── .gitattributes
├── .github/
│   └── workflows/
│       ├── codeql.yml
│       ├── docker.yml
│       ├── lint.yml
│       ├── release.yml
│       ├── tests-sd.yml
│       └── tests.yml
├── .gitignore
├── .golangci.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── autocomplete/
│   ├── autocomplete.go
│   └── autocomplete_test.go
├── cache/
│   └── cache.go
├── capabilities/
│   └── handler.go
├── cmd/
│   ├── e2e-test/
│   │   ├── carbon-clickhouse.go
│   │   ├── checks.go
│   │   ├── clickhouse.go
│   │   ├── container.go
│   │   ├── e2etesting.go
│   │   ├── errors.go
│   │   ├── graphite-clickhouse.go
│   │   ├── main.go
│   │   ├── rproxy.go
│   │   └── utils.go
│   └── graphite-clickhouse-client/
│       └── main.go
├── config/
│   ├── .gitignore
│   ├── config.go
│   ├── config_test.go
│   ├── json.go
│   └── json_test.go
├── deploy/
│   ├── doc/
│   │   ├── .gitignore
│   │   └── config.md
│   └── root/
│       └── usr/
│           └── lib/
│               └── systemd/
│                   └── system/
│                       └── graphite-clickhouse.service
├── doc/
│   ├── aggregation.md
│   ├── config.md
│   ├── debugging.md
│   ├── graphite_clickhouse.gliffy
│   ├── index-table.md
│   └── release.md
├── find/
│   ├── find.go
│   ├── handler.go
│   ├── handler_json_test.go
│   └── handler_test.go
├── finder/
│   ├── base.go
│   ├── blacklist.go
│   ├── date.go
│   ├── date_reverse.go
│   ├── date_reverse_test.go
│   ├── finder.go
│   ├── index.go
│   ├── index_test.go
│   ├── mock.go
│   ├── plain_from_tagged.go
│   ├── plain_from_tagged_test.go
│   ├── prefix.go
│   ├── prefix_test.go
│   ├── reverse.go
│   ├── reverse_test.go
│   ├── split.go
│   ├── split_test.go
│   ├── tag.go
│   ├── tag_test.go
│   ├── tagged.go
│   ├── tagged_test.go
│   ├── tags_count_querier.go
│   └── unescape.go
├── go.mod
├── go.sum
├── graphite-clickhouse.go
├── healthcheck/
│   └── healthcheck.go
├── helper/
│   ├── RowBinary/
│   │   └── encode.go
│   ├── clickhouse/
│   │   ├── clickhouse.go
│   │   ├── clickhouse_test.go
│   │   ├── external-data.go
│   │   └── external-data_test.go
│   ├── client/
│   │   ├── datetime.go
│   │   ├── errros.go
│   │   ├── find.go
│   │   ├── render.go
│   │   ├── requests.go
│   │   ├── tags.go
│   │   └── types.go
│   ├── date/
│   │   ├── date.go
│   │   └── date_test.go
│   ├── datetime/
│   │   ├── datetime.go
│   │   └── datetime_test.go
│   ├── errs/
│   │   └── errors.go
│   ├── headers/
│   │   └── headers.go
│   ├── http/
│   │   └── live-http-client.go
│   ├── pickle/
│   │   └── pickle.go
│   ├── point/
│   │   ├── func.go
│   │   ├── func_test.go
│   │   ├── point.go
│   │   └── points.go
│   ├── rollup/
│   │   ├── aggr.go
│   │   ├── compact.go
│   │   ├── compact_test.go
│   │   ├── remote.go
│   │   ├── remote_test.go
│   │   ├── rollup.go
│   │   ├── rules.go
│   │   ├── rules_test.go
│   │   ├── xml.go
│   │   └── xml_test.go
│   ├── tests/
│   │   ├── clickhouse/
│   │   │   └── server.go
│   │   └── compare/
│   │       ├── compare.go
│   │       └── expand/
│   │           └── expand.go
│   └── utils/
│       ├── utils.go
│       └── utils_test.go
├── index/
│   ├── handler.go
│   ├── index.go
│   └── index_test.go
├── issues/
│   └── daytime/
│       ├── carbon-clickhouse.conf.tpl
│       ├── graphite-clickhouse-internal-aggr.conf.tpl
│       ├── graphite-clickhouse.conf.tpl
│       └── test.toml
├── limiter/
│   ├── alimiter.go
│   ├── alimiter_test.go
│   ├── interface.go
│   ├── limiter.go
│   ├── noop.go
│   └── wlimiter.go
├── load_avg/
│   ├── load_avg.go
│   ├── load_avg_default.go
│   ├── load_avg_linux.go
│   └── load_avg_test.go
├── logs/
│   └── logger.go
├── metrics/
│   ├── limiter_metrics.go
│   ├── metrics.go
│   ├── metrics_test.go
│   ├── query_metrics.go
│   └── statsd.go
├── nfpm.yaml
├── packages.sh
├── pkg/
│   ├── alias/
│   │   ├── map.go
│   │   ├── map_tagged_test.go
│   │   └── map_test.go
│   ├── dry/
│   │   ├── math.go
│   │   ├── math_test.go
│   │   ├── strings.go
│   │   ├── strings_test.go
│   │   ├── unsafe.go
│   │   └── unsafe_test.go
│   ├── reverse/
│   │   ├── reverse.go
│   │   └── reverse_test.go
│   ├── scope/
│   │   ├── context.go
│   │   ├── http_request.go
│   │   ├── key.go
│   │   ├── logger.go
│   │   └── version.go
│   └── where/
│       ├── match.go
│       ├── match_test.go
│       ├── where.go
│       └── where_test.go
├── prometheus/
│   ├── .gitignore
│   ├── empty_iterator.go
│   ├── exemplar.go
│   ├── gatherer.go
│   ├── labels.go
│   ├── labels_test.go
│   ├── local_storage.go
│   ├── logger.go
│   ├── matcher.go
│   ├── metrics_set.go
│   ├── querier.go
│   ├── querier_select.go
│   ├── querier_select_test.go
│   ├── run.go
│   ├── run_dummy.go
│   ├── series_set.go
│   └── storage.go
├── render/
│   ├── data/
│   │   ├── carbonlink.go
│   │   ├── carbonlink_test.go
│   │   ├── ch_response.go
│   │   ├── common_step.go
│   │   ├── common_step_test.go
│   │   ├── data.go
│   │   ├── data_parse_test.go
│   │   ├── multi_target.go
│   │   ├── multi_target_test.go
│   │   ├── query.go
│   │   ├── query_test.go
│   │   ├── targets.go
│   │   └── targets_test.go
│   ├── handler.go
│   ├── handler_test.go
│   └── reply/
│       ├── formatter.go
│       ├── formatter_test.go
│       ├── json.go
│       ├── pickle.go
│       ├── protobuf.go
│       ├── protobuf_test.go
│       ├── v2_pb.go
│       ├── v2_pb_test.go
│       ├── v3_pb.go
│       └── v3_pb_test.go
├── sd/
│   ├── nginx/
│   │   ├── nginx.go
│   │   ├── nginx_test.go
│   │   └── tests/
│   │       └── nginx_cleanup_test.go
│   ├── register.go
│   └── utils/
│       └── utils.go
├── tagger/
│   ├── metric.go
│   ├── rule.go
│   ├── rule_test.go
│   ├── set.go
│   ├── tagger.go
│   ├── tagger_test.go
│   └── tree.go
└── tests/
    ├── agg_internal/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr.conf.tpl
    │   └── test.toml
    ├── agg_latest/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── agg_merge/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── agg_oneblock/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── clickhouse/
    │   ├── rollup/
    │   │   ├── config.xml
    │   │   ├── init.sql
    │   │   ├── rollup.xml
    │   │   └── users.xml
    │   └── rollup_tls/
    │       ├── config.xml
    │       ├── init.sql
    │       ├── rollup.xml
    │       ├── rootCA.crt
    │       ├── server.crt
    │       ├── server.key
    │       └── users.xml
    ├── consolidateBy/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── consul.sh
    ├── emptyseries_append/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── emptyseries_noappend/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── error_handling/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── feature_flags_both_true/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── feature_flags_dont_match_missing_tags/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── feature_flags_false/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── feature_flags_use_carbon_behaviour/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── find_cache/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-cached.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr-cached.conf.tpl
    │   └── test.toml
    ├── limitera/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr-cached.conf.tpl
    │   └── test.toml
    ├── limitermax/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr-cached.conf.tpl
    │   └── test.toml
    ├── limiterw/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr-cached.conf.tpl
    │   └── test.toml
    ├── limiterwn/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr-cached.conf.tpl
    │   └── test.toml
    ├── one_table/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── tags_min_in_query/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── tls/
    │   ├── ca.crt
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── client.crt
    │   ├── client.key
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    └── wildcard_min_distance/
        ├── carbon-clickhouse.conf.tpl
        ├── graphite-clickhouse.conf.tpl
        └── test.toml

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================


================================================
FILE: .github/workflows/codeql.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"

on:
  push:
    branches: [master]
  pull_request:
    # The branches below must be a subset of the branches above
    branches: [master]

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        # Override automatic language detection by changing the below list
        # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
        language: ['go']
        # Learn more...
        # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      with:
        # We must fetch at least the immediate parents so that if this is
        # a pull request then we can checkout the head.
        fetch-depth: 2

    # Initializes the CodeQL tools for scanning.
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v3
      with:
        languages: ${{ matrix.language }}
        # If you wish to specify custom queries, you can do so here or in a config file.
        # By default, queries listed here will override any specified in a config file. 
        # Prefix the list here with "+" to use these queries and those in the config file.
        # queries: ./path/to/local/query, your-org/your-repo/queries@main

    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
    # If this step fails, then you should remove it and run the build manually (see below)
    - name: Autobuild
      uses: github/codeql-action/autobuild@v3

    # ℹ️ Command-line programs to run using the OS shell.
    # 📚 https://git.io/JvXDl

    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
    #    and modify them (or add more) to build your code if your project
    #    uses a compiled language

    #- run: |
    #   make bootstrap
    #   make release

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v3




================================================
FILE: .github/workflows/docker.yml
================================================
name: Docker images

on:
  push:
    branches: [ master ]
    tags: [ 'v*' ]
  pull_request:
    branches: [ master ]

jobs:
  docker:
    name: Build image
    runs-on: ubuntu-latest
    steps:
      - name: Check out code
        uses: actions/checkout@v4
      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v3
        with:
          images: ghcr.io/${{ github.repository }}
          # create latest tag for branch events
          flavor: |
            latest=${{ github.event_name == 'push' && github.ref_type == 'branch' }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}.{{minor}}.{{patch}}
      - name: Login to DockerHub
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v1
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Build and push
        id: docker_build
        uses: docker/build-push-action@v2
        with:
          # push for non-pr events
          push: ${{ github.event_name != 'pull_request' }}
          context: .
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}



================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint
on:
  pull_request:

jobs:
  golangci:
    name: lint
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-go@v4
        with:
          go-version-file: go.mod

      - name: Run linter
        uses: golangci/golangci-lint-action@v7
        with:
          version: v2.0.2



================================================
FILE: .github/workflows/release.yml
================================================
name: Upload Packages to new release

on:
  release:
    types:
      - published

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    env:
      BINARY: ${{ github.event.repository.name }}
      CGO_ENABLED: 0

    outputs:
      matrix: ${{ steps.build.outputs.matrix }}
    steps:
    - name: Set up Go
      uses: actions/setup-go@v5
      with:
        go-version: ^1

    - uses: actions/checkout@v4
      name: Checkout

    - name: Test
      run: make test
      env:
        CGO_ENABLED: 1

    - name: Build packages
      id: build
      run: |
        go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.40.0
        make nfpm-deb nfpm-rpm
        make sum-files
        ARTIFACTS=
        # Upload all deb and rpm packages
        for package in *deb *rpm; do ARTIFACTS=${ARTIFACTS}\"$package\",\ ; done
        echo ::set-output name=matrix::{\"file\": [${ARTIFACTS} \"sha256sum\", \"md5sum\"]}

    - name: Check version
      id: check_version
      run: |
        ./out/${BINARY}-linux-amd64 -version
        [ v$(./out/${BINARY}-linux-amd64 -version) = ${{ github.event.release.tag_name }} ]

    - name: Artifact
      id: artifact
      uses: actions/upload-artifact@v4
      with:
        name: packages
        retention-days: 1
        path: |
          *.deb
          *.rpm
          sha256sum
          md5sum

    - name: Push packages to the stable repo
      run: make packagecloud-stable
      env:
        PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}

  upload:
    needs: build
    runs-on: ubuntu-latest
    strategy:
      matrix: ${{fromJson(needs.build.outputs.matrix)}}
    steps:
    - name: Download artifact
      uses: actions/download-artifact@v4.1.7
      with:
        name: packages
    - name: Upload ${{ matrix.file }}
      id: upload
      uses: actions/upload-release-asset@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ github.event.release.upload_url }}
        asset_path: ${{ matrix.file }}
        asset_name: ${{ matrix.file }}
        asset_content_type: application/octet-stream


================================================
FILE: .github/workflows/tests-sd.yml
================================================
name: Tests register in SD

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:

  tests:
    env:
      CGO_ENABLED: 0
    name: Test register in SD
    runs-on: ubuntu-latest
    strategy:
      matrix:
        go:
          - ^1
    steps:

    - name: Set up Go
      uses: actions/setup-go@v5
      with:
        go-version: ${{ matrix.go }}

    - name: Check out code
      uses: actions/checkout@v4

    - name: Start consul
      run:   |
        ./tests/consul.sh 1.15.2 > /tmp/consul.log &
        sleep 30
      shell: bash

    - name: Test
      run: go test ./sd/nginx -tags=test_sd -v


================================================
FILE: .github/workflows/tests.yml
================================================
name: Tests

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:

  tests:
    env:
      CGO_ENABLED: 0
    name: Test code
    runs-on: ubuntu-latest
    strategy:
      matrix:
        go:
          - ^1.20
          - ^1.21
          - ^1
    steps:

    - name: Set up Go
      uses: actions/setup-go@v5
      with:
        go-version: ${{ matrix.go }}

    - name: Check out code
      uses: actions/checkout@v4

    - name: Checkout to the latest tag
      run: |
        # Fetch all tags
        git fetch --depth=1 --tags
        # Get the latest tag
        VERS=$(git tag -l | sort -Vr | head -n1)
        # Fetch everything to the latest tag
        git fetch --shallow-since=$(git log $VERS -1 --format=%at)
      if: ${{ github.event_name == 'push' }} # only when built from master

    - name: Build project
      run: make

    - name: Validate default configs
      run: |
        ./graphite-clickhouse -config-print-default > /tmp/graphite-clickhouse.conf
        ./graphite-clickhouse -config /tmp/graphite-clickhouse.conf -check-config

    - name: Check documentation consistency
      run: |
        make config
        git diff --exit-code

    - name: Test
      run: make test
      env:
        CGO_ENABLED: 1

    - name: Test (with GMT-5)
      run: |
        go clean -testcache
        TZ=Etc/GMT-5 make test
      env:
        CGO_ENABLED: 1

    - name: Test (with GMT+5)
      run: |
        go clean -testcache
        TZ=Etc/GMT+5 make test
      env:
        CGO_ENABLED: 1

    - name: Integration tests
      run: |
        make e2e-test
        ./e2e-test -config tests -abort -rmi

    # TODO (msaf1980): find a way to set TZ in carbon-clickhouse docker (or run locally)
    # run with clickhouse.date-format = "both"
    # - name: Integration tests (with Etc/GMT-5)
    #   run: |
    #     make e2e-test
    #     sudo timedatectl set-timezone Etc/GMT-5
    #     ./e2e-test -config issues/daytime

    # - name: Integration tests (with Etc/GMT+5)
    #   run: |
    #     make e2e-test
    #     sudo timedatectl set-timezone Etc/GMT+5
    #     ./e2e-test -config issues/daytime

    - name: Check packaging
      run: |
        go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.40.0
        make DEVEL=1 nfpm-deb nfpm-rpm
        make sum-files

    - name: Artifact
      id: artifact
      uses: actions/upload-artifact@v4
      with:
        name: packages-${{ matrix.go }}
        path: |
          *.deb
          *.rpm
          sha256sum
          md5sum

    - name: Push packages to the autobuilds repo
      if: ${{ github.event_name == 'push' && matrix.go == '^1' }} # only when built from master with latest go
      run: make DEVEL=1 packagecloud-autobuilds
      env:
        PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}


================================================
FILE: .gitignore
================================================
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
graphite-clickhouse
.vscode
/out/

# Folders
_obj
_test

# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out

*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*

_testmain.go

*.exe
*.test
*.prof


================================================
FILE: .golangci.yml
================================================
version: "2"
linters:
  default: none
  enable:
    - asasalint
    - asciicheck
    - bidichk
    - bodyclose
#    - contextcheck
    - decorder
#    - dogsled
    - durationcheck
#    - errcheck
#    - errorlint
#    - fatcontext
    - ginkgolinter
    - gocheckcompilerdirectives
    - gochecksumtype
#    - goconst
#    - gocyclo
#    - godot
    - goheader
    - govet
    - grouper
#    - ineffassign
    - loggercheck
#    - makezero
#    - misspell
#    - mnd
#    - nilerr
#    - noctx
#    - nosprintfhostport
    - prealloc
#    - predeclared
    - promlinter
    - protogetter
    - reassign
#    - revive
    - rowserrcheck
    - sloglint
    - spancheck
    - sqlclosecheck
#    - staticcheck
#    - testifylint
    - tparallel
    - unconvert
#    - unparam
#    - unused
    - usestdlibvars
#    - wastedassign
    - whitespace
    - wsl
  settings:
    gocyclo:
      min-complexity: 15
    govet:
      settings:
        printf:
          funcs:
            - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
            - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
            - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
            - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
    unparam:
      check-exported: false
  exclusions:
    generated: lax
    presets:
      - comments
      - common-false-positives
      - legacy
      - std-error-handling
    rules:
      - linters:
          - errcheck
          - contextcheck
          - goconst
          - mnd
        path: _test\.go
      - linters:
          - godot
        path: notifier/registrator.go
    paths:
      - third_party$
      - builtin$
      - examples$
formatters:
  enable:
    - gofmt
#    - gofumpt
    - goimports
  exclusions:
    generated: lax
    paths:
      - third_party$
      - builtin$
      - examples$


================================================
FILE: Dockerfile
================================================
FROM golang:alpine as builder

WORKDIR /go/src/github.com/lomik/graphite-clickhouse
COPY . .

ENV GOPATH=/go

RUN apk add git --no-cache

RUN go build -ldflags '-extldflags "-static"' github.com/lomik/graphite-clickhouse

FROM alpine:latest

RUN apk --no-cache add ca-certificates
WORKDIR /

COPY --from=builder /go/src/github.com/lomik/graphite-clickhouse/graphite-clickhouse /usr/bin/graphite-clickhouse

CMD ["graphite-clickhouse"]



================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2016 Roman Lomonosov

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
NAME:=graphite-clickhouse
DESCRIPTION:="Graphite cluster backend with ClickHouse support"
MODULE:=github.com/lomik/graphite-clickhouse

GO ?= go
export GO111MODULE := on
TEMPDIR:=$(shell mktemp -d)

DEVEL ?= 0
ifeq ($(DEVEL), 0)
VERSION:=$(shell sh -c 'grep "const Version" $(NAME).go  | cut -d\" -f2')
else
VERSION:=$(shell sh -c 'git describe --always --tags | sed -e "s/^v//i"')
endif

SRCS:=$(shell find . -name '*.go')

all: $(NAME)

.PHONY: clean
clean:
	rm -f $(NAME) $(NAME)-client
	rm -rf out
	rm -f *deb *rpm
	rm -f sha256sum md5sum

$(NAME): $(SRCS)
	$(GO) build -ldflags '-X main.BuildVersion=$(VERSION)' $(MODULE) 

debug: $(SRCS)
	$(GO) build -ldflags '-X main.BuildVersion=$(VERSION)' -gcflags=all='-N -l' $(MODULE)

deploy/doc/graphite-clickhouse.conf: $(NAME)
	./$(NAME) -config-print-default > $@

doc/config.md: deploy/doc/graphite-clickhouse.conf deploy/doc/config.md
	@echo 'Generating $@...'
	@printf '[//]: # (This file is built out of deploy/doc/config.md, please do not edit it manually)  \n' > $@
	@printf '[//]: # (To rebuild it run `make config`)\n\n' >> $@
	@cat deploy/doc/config.md >> $@
	@printf '\n```toml\n' >> $@
	@cat deploy/doc/graphite-clickhouse.conf >> $@
	@printf '```\n' >> $@

config: doc/config.md

# run after prometheus upgrade
prometheus/ui:
	vendor_prometheus_ui.sh

test:
	$(GO) test -race ./...

e2e-test: $(NAME)
	$(GO) build $(MODULE)/cmd/e2e-test

client: $(NAME)
	$(GO) build $(MODULE)/cmd/graphite-clickhouse-client

gox-build: out/$(NAME)-linux-amd64 out/$(NAME)-linux-arm64 out/root/etc/$(NAME)/$(NAME).conf

ARCH = amd64 arm64
out/$(NAME)-linux-%: out $(SRCS)
	GOOS=linux GOARCH=$* $(GO) build -ldflags '-X main.BuildVersion=$(VERSION)' -o $@ $(MODULE)

out:
	mkdir -p out

out/root/etc/$(NAME)/$(NAME).conf: $(NAME)
	mkdir -p "$(shell dirname $@)"
	./$(NAME) -config-print-default > $@

nfpm-deb: gox-build
	$(MAKE) nfpm-build-deb ARCH=amd64
	$(MAKE) nfpm-build-deb ARCH=arm64
nfpm-rpm: gox-build
	$(MAKE) nfpm-build-rpm ARCH=amd64
	$(MAKE) nfpm-build-rpm ARCH=arm64

nfpm-build-%: nfpm.yaml
	NAME=$(NAME) DESCRIPTION=$(DESCRIPTION) ARCH=$(ARCH) VERSION_STRING=$(VERSION) nfpm package --packager $*

.ONESHELL:
RPM_VERSION:=$(subst -,_,$(VERSION))
packagecloud-push-rpm: $(wildcard $(NAME)-$(RPM_VERSION)-1.*.rpm)
	for pkg in $^; do
		package_cloud push $(REPO)/el/7 $${pkg} || true
		package_cloud push $(REPO)/el/8 $${pkg} || true
		package_cloud push $(REPO)/el/9 $${pkg} || true
	done

.ONESHELL:
packagecloud-push-deb: $(wildcard $(NAME)_$(VERSION)_*.deb)
	for pkg in $^; do
		package_cloud push $(REPO)/ubuntu/xenial   $${pkg} || true
		package_cloud push $(REPO)/ubuntu/bionic   $${pkg} || true
		package_cloud push $(REPO)/ubuntu/focal    $${pkg} || true
		package_cloud push $(REPO)/debian/stretch  $${pkg} || true
		package_cloud push $(REPO)/debian/buster   $${pkg} || true
		package_cloud push $(REPO)/debian/bullseye $${pkg} || true
	done

packagecloud-push:
	@$(MAKE) packagecloud-push-rpm
	@$(MAKE) packagecloud-push-deb

packagecloud-autobuilds:
	$(MAKE) packagecloud-push REPO=go-graphite/autobuilds

packagecloud-stable:
	$(MAKE) packagecloud-push REPO=go-graphite/stable

sum-files: | sha256sum md5sum

md5sum:
	md5sum $(wildcard $(NAME)_$(VERSION)*.deb) $(wildcard $(NAME)-$(VERSION)*.rpm) > md5sum

sha256sum:
	sha256sum $(wildcard $(NAME)_$(VERSION)*.deb) $(wildcard $(NAME)-$(VERSION)*.rpm) > sha256sum

.PHONY: lint
lint:
	golangci-lint run


================================================
FILE: README.md
================================================
[![deb](https://img.shields.io/badge/deb-packagecloud.io-844fec.svg)](https://packagecloud.io/go-graphite/stable)
[![rpm](https://img.shields.io/badge/rpm-packagecloud.io-844fec.svg)](https://packagecloud.io/go-graphite/stable)

# graphite-clickhouse
Graphite cluster backend with ClickHouse support

## Work scheme
![stack.png](doc/stack.png?v3)

Gray components are optional or alternative

## TL;DR
[Preconfigured docker-compose](https://github.com/lomik/graphite-clickhouse-tldr)

### Docker
Docker images are available on [packages](https://github.com/lomik/graphite-clickhouse/pkgs/container/graphite-clickhouse) page.

## Compatibility
- [x] [graphite-web 1.1.0](https://github.com/graphite-project/graphite-web)
- [x] [graphite-web 0.9.15](https://github.com/graphite-project/graphite-web/tree/0.9.15)
- [x] [graphite-web 1.0.0](https://github.com/graphite-project/graphite-web)
- [x] [carbonapi 0.14.1+](https://github.com/go-graphite/carbonapi)
- [x] [carbonzipper](https://github.com/go-graphite/carbonzipper) (DEPRECATED, is part of carbonapi currently)

## Build
Required golang 1.18+
```sh
# build binary
git clone https://github.com/lomik/graphite-clickhouse.git
cd graphite-clickhouse
make
```

## Installation
1. Setup [Yandex ClickHouse](https://github.com/yandex/ClickHouse) and [carbon-clickhouse](https://github.com/lomik/carbon-clickhouse)
2. Setup and configure `graphite-clickhouse`
3. Add graphite-clickhouse `host:port` to graphite-web [CLUSTER_SERVERS](http://graphite.readthedocs.io/en/latest/config-local-settings.html#cluster-configuration)

## Configuration
See [configuration documentation](./doc/config.md).

### Special headers processing

Some HTTP headers are processed specially by the service

#### Request headers

*Grafana headers*: `X-Dashboard-Id`, `X-Grafana-Org-Id`, and `X-Panel-Id` are logged and passed further to the ClickHouse.

*Debug headers* (see [debugging.md](./doc/debugging.md) for details):

- `X-Gch-Debug-External-Data` - when this header is set to anything and every of `directory`, `directory-perm`, and `external-data-perm` parameters in `[debug]` is set and valid, service will save the dump of external data tables in the directory for debug output.
- `X-Gch-Debug-Output` - header to enable special processing for `format=carbonapi_v3_pb` and `format=json` render output.
- `X-Gch-Debug-Protobuf` - header enables the original marshallers for `protobuf` and `carbonapi_v3_pb` to check the binary data integrity.

#### Response headers

- `X-Gch-Request-Id` - the current request ID.
- `X-Cached-Find`    - Flag for find cache hit.

## Run on same host with old graphite-web 0.9.x
By default graphite-web won't connect to CLUSTER_SERVER on localhost. Cheat:
```python
class ForceLocal(str):
    def split(self, *args, **kwargs):
        return ["8.8.8.8", "8080"]

CLUSTER_SERVERS = [ForceLocal("127.0.0.1:9090")]
```


================================================
FILE: autocomplete/autocomplete.go
================================================
package autocomplete

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/go-graphite/carbonapi/pkg/parser"
	"github.com/msaf1980/go-stringutils"
	"go.uber.org/zap"

	"github.com/lomik/graphite-clickhouse/config"
	"github.com/lomik/graphite-clickhouse/finder"
	"github.com/lomik/graphite-clickhouse/helper/clickhouse"
	"github.com/lomik/graphite-clickhouse/helper/date"
	"github.com/lomik/graphite-clickhouse/helper/utils"
	"github.com/lomik/graphite-clickhouse/logs"
	"github.com/lomik/graphite-clickhouse/metrics"
	"github.com/lomik/graphite-clickhouse/pkg/scope"
	"github.com/lomik/graphite-clickhouse/pkg/where"
)

// override in unit tests for stable results
var timeNow = time.Now

type Handler struct {
	config   *config.Config
	isValues bool
}

func NewTags(config *config.Config) *Handler {
	h := &Handler{
		config: config,
	}

	return h
}

func NewValues(config *config.Config) *Handler {
	h := &Handler{
		config:   config,
		isValues: true,
	}

	return h
}

func dateString(autocompleteDays int, tm time.Time) (string, string) {
	fromDate := date.FromTimeToDaysFormat(tm.AddDate(0, 0, -autocompleteDays))
	untilDate := date.UntilTimeToDaysFormat(tm)

	return fromDate, untilDate
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Don't process, if the tagged table is not set
	if h.config.ClickHouse.TaggedTable == "" {
		w.Write([]byte{'[', ']'})
		return
	}

	if h.isValues {
		h.ServeValues(w, r)
	} else {
		h.ServeTags(w, r)
	}
}

func getTagCountQuerier(config *config.Config, opts clickhouse.Options) *finder.TagCountQuerier {
	var tcq *finder.TagCountQuerier = nil
	if config.ClickHouse.TagsCountTable != "" {
		tcq = finder.NewTagCountQuerier(
			config.ClickHouse.URL,
			config.ClickHouse.TagsCountTable,
			opts,
			config.FeatureFlags.UseCarbonBehavior,
			config.FeatureFlags.DontMatchMissingTags,
			config.ClickHouse.TaggedUseDaily,
		)
	}

	return tcq
}

func (h *Handler) requestExpr(r *http.Request, tcq *finder.TagCountQuerier, from, until int64) (*where.Where, *where.Where, map[string]bool, error) {
	formExpr := r.Form["expr"]
	expr := make([]string, 0, len(formExpr))

	for i := 0; i < len(formExpr); i++ {
		if formExpr[i] != "" {
			expr = append(expr, formExpr[i])
		}
	}

	usedTags := make(map[string]bool)

	wr := where.New()
	pw := where.New()

	if len(expr) == 0 {
		return wr, pw, usedTags, nil
	}

	terms, err := finder.ParseTaggedConditions(expr, h.config, true)
	if err != nil {
		return wr, pw, usedTags, err
	}

	if tcq != nil {
		tagValuesCosts, err := tcq.GetCostsFromCountTable(r.Context(), terms, from, until)
		if err != nil {
			return wr, pw, usedTags, err
		}

		if tagValuesCosts != nil {
			finder.SetCosts(terms, tagValuesCosts)
		} else if len(h.config.ClickHouse.TaggedCosts) != 0 {
			finder.SetCosts(terms, h.config.ClickHouse.TaggedCosts)
		}
	}

	finder.SortTaggedTermsByCost(terms)

	wr, pw, err = finder.TaggedWhere(terms, h.config.FeatureFlags.UseCarbonBehavior, h.config.FeatureFlags.DontMatchMissingTags)
	if err != nil {
		return wr, pw, usedTags, err
	}

	for i := 0; i < len(expr); i++ {
		a := strings.Split(expr[i], "=")
		usedTags[a[0]] = true
	}

	return wr, pw, usedTags, nil
}

func taggedKey(typ string, truncateSec int32, fromDate, untilDate string, tag string, exprs []string, tagPrefix string, limit int) (string, string) {
	ts := utils.TimestampTruncate(timeNow().Unix(), time.Duration(truncateSec)*time.Second)

	var sb stringutils.Builder

	sb.Grow(128)
	sb.WriteString(typ)
	sb.WriteString(fromDate)
	sb.WriteByte(';')
	sb.WriteString(untilDate)
	sb.WriteString(";limit=")
	sb.WriteInt(int64(limit), 10)
	tagStart := sb.Len()

	if tagPrefix != "" {
		sb.WriteString(";tagPrefix=")
		sb.WriteString(tagPrefix)
	}

	if tag != "" {
		sb.WriteString(";tag=")
		sb.WriteString(tag)
	}

	for _, expr := range exprs {
		sb.WriteString(";expr='")
		sb.WriteString(strings.Replace(expr, " = ", "=", 1))
		sb.WriteByte('\'')
	}

	exprEnd := sb.Len()
	sb.WriteString(";ts=")
	sb.WriteString(strconv.FormatInt(ts, 10))

	s := sb.String()

	return s, s[tagStart:exprEnd]
}

func taggedValuesKey(typ string, truncateSec int32, fromDate, untilDate string, tag string, exprs []string, valuePrefix string, limit int) (string, string) {
	ts := utils.TimestampTruncate(timeNow().Unix(), time.Duration(truncateSec)*time.Second)

	var sb stringutils.Builder

	sb.Grow(128)
	sb.WriteString(typ)
	sb.WriteString(fromDate)
	sb.WriteByte(';')
	sb.WriteString(untilDate)
	sb.WriteString(";limit=")
	sb.WriteInt(int64(limit), 10)
	tagStart := sb.Len()

	if valuePrefix != "" {
		sb.WriteString(";valuePrefix=")
		sb.WriteString(valuePrefix)
	}

	if tag != "" {
		sb.WriteString(";tag=")
		sb.WriteString(tag)
	}

	for _, expr := range exprs {
		sb.WriteString(";expr='")
		sb.WriteString(strings.Replace(expr, " = ", "=", 1))
		sb.WriteByte('\'')
	}

	exprEnd := sb.Len()
	sb.WriteString(";ts=")
	sb.WriteString(strconv.FormatInt(ts, 10))

	s := sb.String()

	return s, s[tagStart:exprEnd]
}

// func taggedTagsQuery(exprs []string, tagPrefix string, limit int) []string {
// 	query := make([]string, 0, 3+len(exprs))
// 	if tagPrefix != "" {
// 		query = append(query, "tagPrefix="+tagPrefix)
// 	}
// 	for _, expr := range exprs {
// 		query = append(query, "expr='"+expr+"'")
// 	}
// 	query = append(query, "limit="+strconv.Itoa(limit))
// 	return query
// }

func (h *Handler) ServeTags(w http.ResponseWriter, r *http.Request) {
	start := timeNow()
	status := http.StatusOK
	accessLogger := scope.LoggerWithHeaders(r.Context(), r, h.config.Common.HeadersToLog).Named("http")
	logger := scope.LoggerWithHeaders(r.Context(), r, h.config.Common.HeadersToLog).Named("autocomplete")
	r = r.WithContext(scope.WithLogger(r.Context(), logger))

	var (
		err           error
		chReadRows    int64
		chReadBytes   int64
		metricsCount  int64
		readBytes     int64
		queueFail     bool
		queueDuration time.Duration
		findCache     bool
		opts          clickhouse.Options
	)

	username := r.Header.Get("X-Forwarded-User")
	limiter := h.config.GetUserTagsLimiter(username)

	defer func() {
		if rec := recover(); rec != nil {
			status = http.StatusInternalServerError

			logger.Error("panic during eval:",
				zap.String("requestID", scope.String(r.Context(), "requestID")),
				zap.Any("reason", rec),
				zap.Stack("stack"),
			)

			answer := fmt.Sprintf("%v\nStack trace: %v", rec, zap.Stack("").String)
			http.Error(w, answer, status)
		}

		d := time.Since(start)
		dMS := d.Milliseconds()
		logs.AccessLog(accessLogger, h.config, r, status, d, queueDuration, findCache, queueFail)
		limiter.SendDuration(queueDuration.Milliseconds())
		metrics.SendFindMetrics(metrics.TagsRequestMetric, status, dMS, 0, h.config.Metrics.ExtendedStat, metricsCount)

		if !findCache && chReadRows > 0 && chReadBytes > 0 {
			errored := status != http.StatusOK && status != http.StatusNotFound
			metrics.SendQueryRead(metrics.AutocompleteQMetric, 0, 0, dMS, metricsCount, readBytes, chReadRows, chReadBytes, errored)
		}
	}()

	r.ParseMultipartForm(1024 * 1024)
	tagPrefix := r.FormValue("tagPrefix")
	limitStr := r.FormValue("limit")
	limit := 10000

	var body []byte

	if limitStr != "" {
		limit, err = strconv.Atoi(limitStr)
		if err == finder.ErrCostlySeriesByTag {
			status = http.StatusForbidden
			http.Error(w, err.Error(), status)

			return
		} else if err != nil {
			status = http.StatusBadRequest
			http.Error(w, err.Error(), status)

			return
		}
	}

	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, start)

	var key string

	exprs := r.Form["expr"]
	// params := taggedTagsQuery(exprs, tagPrefix, limit)

	useCache := h.config.Common.FindCache != nil && h.config.Common.FindCacheConfig.FindTimeoutSec > 0 && !parser.TruthyBool(r.FormValue("noCache"))
	if useCache {
		key, _ = taggedKey("tags;", h.config.Common.FindCacheConfig.FindTimeoutSec, fromDate, untilDate, "", exprs, tagPrefix, limit)

		body, err = h.config.Common.FindCache.Get(key)
		if err == nil {
			if metrics.FinderCacheMetrics != nil {
				metrics.FinderCacheMetrics.CacheHits.Add(1)
			}

			findCache = true

			w.Header().Set("X-Cached-Find", strconv.Itoa(int(h.config.Common.FindCacheConfig.FindTimeoutSec)))
		}
	}

	opts = clickhouse.Options{
		TLSConfig:               h.config.ClickHouse.TLSConfig,
		Timeout:                 h.config.ClickHouse.IndexTimeout,
		ConnectTimeout:          h.config.ClickHouse.ConnectTimeout,
		CheckRequestProgress:    h.config.FeatureFlags.LogQueryProgress,
		ProgressSendingInterval: h.config.ClickHouse.ProgressSendingInterval,
	}

	wr, pw, usedTags, err := h.requestExpr(
		r,
		getTagCountQuerier(h.config, opts),
		start.AddDate(0, 0, -h.config.ClickHouse.TaggedAutocompleDays).Unix(),
		start.Unix(),
	)

	if err != nil {
		status = http.StatusBadRequest
		http.Error(w, err.Error(), status)

		return
	}

	if !findCache {
		var valueSQL string

		if len(usedTags) == 0 {
			valueSQL = "splitByChar('=', Tag1)[1] AS value"

			if tagPrefix != "" {
				wr.And(where.HasPrefix("Tag1", tagPrefix))
			}
		} else {
			valueSQL = "splitByChar('=', arrayJoin(Tags))[1] AS value"

			if tagPrefix != "" {
				wr.And(where.HasPrefix("arrayJoin(Tags)", tagPrefix))
			}
		}

		queryLimit := limit + len(usedTags)

		wr.Andf("Date >= '%s' AND Date <= '%s'", fromDate, untilDate)

		sql := fmt.Sprintf("SELECT %s FROM %s %s %s GROUP BY value ORDER BY value LIMIT %d",
			valueSQL,
			h.config.ClickHouse.TaggedTable,
			pw.PreWhereSQL(),
			wr.SQL(),
			queryLimit,
		)

		var (
			entered bool
			ctx     context.Context
			cancel  context.CancelFunc
		)

		if limiter.Enabled() {
			ctx, cancel = context.WithTimeout(context.Background(), h.config.ClickHouse.IndexTimeout)
			defer cancel()

			err = limiter.Enter(ctx, "tags")
			queueDuration = time.Since(start)

			if err != nil {
				status = http.StatusServiceUnavailable
				queueFail = true

				logger.Error(err.Error())
				http.Error(w, err.Error(), status)

				return
			}

			queueDuration = time.Since(start)
			entered = true

			defer func() {
				if entered {
					limiter.Leave(ctx, "tags")

					entered = false
				}
			}()
		}

		body, chReadRows, chReadBytes, err = clickhouse.Query(
			scope.WithTable(r.Context(), h.config.ClickHouse.TaggedTable),
			h.config.ClickHouse.URL,
			sql,
			opts,
			nil,
		)

		if entered {
			// release early as possible
			limiter.Leave(ctx, "tags")

			entered = false
		}

		if err != nil {
			status, _ = clickhouse.HandleError(w, err)
			return
		}

		readBytes = int64(len(body))

		if useCache {
			if metrics.FinderCacheMetrics != nil {
				metrics.FinderCacheMetrics.CacheMisses.Add(1)
			}

			h.config.Common.FindCache.Set(key, body, h.config.Common.FindCacheConfig.FindTimeoutSec)
		}
	}

	rows := strings.Split(stringutils.UnsafeString(body), "\n")
	tags := make([]string, 0, uint64(len(rows))+1) // +1 - reserve for "name" tag

	hasName := false

	for i := 0; i < len(rows); i++ {
		if rows[i] == "" {
			continue
		}

		if rows[i] == "__name__" {
			rows[i] = "name"
		}

		if usedTags[rows[i]] {
			continue
		}

		tags = append(tags, rows[i])

		if rows[i] == "name" {
			hasName = true
		}
	}

	if !hasName && !usedTags["name"] && (tagPrefix == "" || strings.HasPrefix("name", tagPrefix)) {
		tags = append(tags, "name")
	}

	sort.Strings(tags)

	if len(tags) > limit {
		tags = tags[:limit]
	}

	if useCache {
		if findCache {
			logger.Info("finder", zap.String("get_cache", key),
				zap.Int("metrics", len(rows)), zap.Bool("find_cached", true),
				zap.Int32("ttl", h.config.Common.FindCacheConfig.FindTimeoutSec))
		} else {
			logger.Info("finder", zap.String("set_cache", key),
				zap.Int("metrics", len(rows)), zap.Bool("find_cached", false),
				zap.Int32("ttl", h.config.Common.FindCacheConfig.FindTimeoutSec))
		}
	}

	b, err := json.Marshal(tags)
	if err != nil {
		status = http.StatusInternalServerError
		http.Error(w, err.Error(), status)

		return
	}

	metricsCount = int64(len(tags))

	w.Write(b)
}

// func taggedValuesQuery(tag string, exprs []string, valuePrefix string, limit int) []string {
// 	query := make([]string, 0, 3+len(exprs))
// 	if tag != "" {
// 		query = append(query, "tag="+tag)
// 	}
// 	if valuePrefix != "" {
// 		query = append(query, "valuePrefix="+valuePrefix)
// 	}
// 	for _, expr := range exprs {
// 		query = append(query, "expr='"+expr+"'")
// 	}
// 	query = append(query, "limit="+strconv.Itoa(limit))
// 	return query
// }

func (h *Handler) ServeValues(w http.ResponseWriter, r *http.Request) {
	start := timeNow()
	status := http.StatusOK
	accessLogger := scope.LoggerWithHeaders(r.Context(), r, h.config.Common.HeadersToLog).Named("http")
	logger := scope.LoggerWithHeaders(r.Context(), r, h.config.Common.HeadersToLog).Named("autocomplete")
	r = r.WithContext(scope.WithLogger(r.Context(), logger))

	var (
		err           error
		body          []byte
		chReadRows    int64
		chReadBytes   int64
		metricsCount  int64
		queueFail     bool
		queueDuration time.Duration
		findCache     bool
		opts          clickhouse.Options
	)

	username := r.Header.Get("X-Forwarded-User")
	limiter := h.config.GetUserTagsLimiter(username)

	defer func() {
		if rec := recover(); rec != nil {
			status = http.StatusInternalServerError

			logger.Error("panic during eval:",
				zap.String("requestID", scope.String(r.Context(), "requestID")),
				zap.Any("reason", rec),
				zap.Stack("stack"),
			)

			answer := fmt.Sprintf("%v\nStack trace: %v", rec, zap.Stack("").String)
			http.Error(w, answer, status)
		}

		d := time.Since(start)
		dMS := d.Milliseconds()
		logs.AccessLog(accessLogger, h.config, r, status, d, queueDuration, findCache, queueFail)
		limiter.SendDuration(queueDuration.Milliseconds())
		metrics.SendFindMetrics(metrics.TagsRequestMetric, status, dMS, 0, h.config.Metrics.ExtendedStat, metricsCount)

		if !findCache && chReadRows > 0 && chReadBytes > 0 {
			errored := status != http.StatusOK && status != http.StatusNotFound
			metrics.SendQueryRead(metrics.AutocompleteQMetric, 0, 0, dMS, metricsCount, int64(len(body)), chReadRows, chReadBytes, errored)
		}
	}()

	r.ParseMultipartForm(1024 * 1024)

	tag := r.FormValue("tag")
	if tag == "name" {
		tag = "__name__"
	}

	valuePrefix := r.FormValue("valuePrefix")
	limitStr := r.FormValue("limit")
	limit := 10000

	if limitStr != "" {
		limit, err = strconv.Atoi(limitStr)
		if err != nil {
			status = http.StatusBadRequest
			http.Error(w, err.Error(), status)

			return
		}
	}

	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, start)

	var key string

	exprs := r.Form["expr"]
	// params := taggedValuesQuery(tag, exprs, valuePrefix, limit)

	// taggedKey(tag, , "valuePrefix="+valuePrefix, limit)
	useCache := h.config.Common.FindCache != nil && h.config.Common.FindCacheConfig.FindTimeoutSec > 0 && !parser.TruthyBool(r.FormValue("noCache"))
	if useCache {
		// logger = logger.With(zap.String("use_cache", "true"))
		key, _ = taggedValuesKey("values;", h.config.Common.FindCacheConfig.FindTimeoutSec, fromDate, untilDate, tag, exprs, valuePrefix, limit)

		body, err = h.config.Common.FindCache.Get(key)
		if err == nil {
			if metrics.FinderCacheMetrics != nil {
				metrics.FinderCacheMetrics.CacheHits.Add(1)
			}

			findCache = true

			w.Header().Set("X-Cached-Find", strconv.Itoa(int(h.config.Common.FindCacheConfig.FindTimeoutSec)))
		}
	}

	opts = clickhouse.Options{
		TLSConfig:               h.config.ClickHouse.TLSConfig,
		Timeout:                 h.config.ClickHouse.IndexTimeout,
		ConnectTimeout:          h.config.ClickHouse.ConnectTimeout,
		CheckRequestProgress:    h.config.FeatureFlags.LogQueryProgress,
		ProgressSendingInterval: h.config.ClickHouse.ProgressSendingInterval,
	}

	if !findCache {
		wr, pw, usedTags, err := h.requestExpr(
			r,
			getTagCountQuerier(h.config, opts),
			start.AddDate(0, 0, -h.config.ClickHouse.TaggedAutocompleDays).Unix(),
			start.Unix(),
		)

		if err == finder.ErrCostlySeriesByTag {
			status = http.StatusForbidden
			http.Error(w, err.Error(), status)

			return
		} else if err != nil {
			status = http.StatusBadRequest
			http.Error(w, err.Error(), status)

			return
		}

		var valueSQL string
		if len(usedTags) == 0 {
			valueSQL = fmt.Sprintf("substr(Tag1, %d) AS value", len(tag)+2)
			wr.And(where.HasPrefix("Tag1", tag+"="+valuePrefix))
		} else {
			prefixSelector := where.HasPrefix("x", tag+"="+valuePrefix)
			valueSQL = fmt.Sprintf("substr(arrayFilter(x -> %s, Tags)[1], %d) AS value", prefixSelector, len(tag)+2)
			wr.And("arrayExists(x -> " + prefixSelector + ", Tags)")
		}

		wr.Andf("Date >= '%s' AND Date <= '%s'", fromDate, untilDate)

		sql := fmt.Sprintf("SELECT %s FROM %s %s %s GROUP BY value ORDER BY value LIMIT %d",
			valueSQL,
			h.config.ClickHouse.TaggedTable,
			pw.PreWhereSQL(),
			wr.SQL(),
			limit,
		)

		var (
			entered bool
			ctx     context.Context
			cancel  context.CancelFunc
		)

		if limiter.Enabled() {
			ctx, cancel = context.WithTimeout(context.Background(), h.config.ClickHouse.IndexTimeout)
			defer cancel()

			err = limiter.Enter(ctx, "tags")
			queueDuration = time.Since(start)

			if err != nil {
				status = http.StatusServiceUnavailable
				queueFail = true

				logger.Error(err.Error())
				http.Error(w, err.Error(), status)

				return
			}

			queueDuration = time.Since(start)
			entered = true

			defer func() {
				if entered {
					limiter.Leave(ctx, "tags")

					entered = false
				}
			}()
		}

		body, chReadRows, chReadBytes, err = clickhouse.Query(
			scope.WithTable(r.Context(), h.config.ClickHouse.TaggedTable),
			h.config.ClickHouse.URL,
			sql,
			opts,
			nil,
		)

		if entered {
			// release early as possible
			limiter.Leave(ctx, "tags")

			entered = false
		}

		if err != nil {
			status, _ = clickhouse.HandleError(w, err)
			return
		}

		if useCache {
			if metrics.FinderCacheMetrics != nil {
				metrics.FinderCacheMetrics.CacheMisses.Add(1)
			}

			h.config.Common.FindCache.Set(key, body, h.config.Common.FindCacheConfig.FindTimeoutSec)
		}
	}

	var rows []string
	if len(body) > 0 {
		rows = strings.Split(stringutils.UnsafeString(body), "\n")
		if len(rows) > 0 && rows[len(rows)-1] == "" {
			rows = rows[:len(rows)-1]
		}

		metricsCount = int64(len(rows))
	}

	if useCache {
		if findCache {
			logger.Info("finder", zap.String("get_cache", key),
				zap.Int("metrics", len(rows)), zap.Bool("find_cached", true),
				zap.Int32("ttl", h.config.Common.FindCacheConfig.FindTimeoutSec))
		} else {
			logger.Info("finder", zap.String("set_cache", key),
				zap.Int("metrics", len(rows)), zap.Bool("find_cached", false),
				zap.Int32("ttl", h.config.Common.FindCacheConfig.FindTimeoutSec))
		}
	}

	b, err := json.Marshal(rows)
	if err != nil {
		status = http.StatusInternalServerError
		http.Error(w, err.Error(), status)

		return
	}

	w.Write(b)
}


================================================
FILE: autocomplete/autocomplete_test.go
================================================
package autocomplete

import (
	"io"
	"net/http"
	"net/http/httptest"
	"strconv"
	"testing"
	"time"

	"github.com/lomik/graphite-clickhouse/config"
	"github.com/lomik/graphite-clickhouse/helper/date"
	chtest "github.com/lomik/graphite-clickhouse/helper/tests/clickhouse"
	"github.com/lomik/graphite-clickhouse/metrics"
	"github.com/stretchr/testify/assert"
)

func NewRequest(method, url string, body io.Reader) *http.Request {
	r, _ := http.NewRequest(method, url, body)

	return r
}

type testStruct struct {
	request     *http.Request
	wantCode    int
	want        string
	wantContent string
}

func testResponce(t *testing.T, step int, h *Handler, tt *testStruct, wantCachedFind string) {
	w := httptest.NewRecorder()

	h.ServeHTTP(w, tt.request)

	s := w.Body.String()

	assert.Equalf(t, tt.wantCode, w.Code, "code mismatch step %d\n,%s", step, s)

	if w.Code == http.StatusOK {
		if tt.wantContent != "" {
			contentType := w.Header().Get("Content-Type")
			assert.Equalf(t, tt.wantContent, contentType, "content type mismatch, step %d", step)
		}

		cachedFindHeader := w.Header().Get("X-Cached-Find")
		assert.Equalf(t, cachedFindHeader, wantCachedFind, "cached find '%s' mismatch, want be %v, step %d", cachedFindHeader, wantCachedFind, step)

		assert.Equalf(t, tt.want, s, "Step %d", step)
	}
}

func TestHandler_ServeTags(t *testing.T) {
	timeNow = func() time.Time {
		return time.Unix(1669714247, 0)
	}

	metrics.DisableMetrics()

	srv := chtest.NewTestServer()
	defer srv.Close()

	cfg, _ := config.DefaultConfig()
	cfg.ClickHouse.URL = srv.URL
	cfg.ClickHouse.TaggedTable = "graphite_tagged"

	h := NewTags(cfg)

	now := timeNow()
	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, now)

	// Test 1: Get all tags without filters
	srv.AddResponce(
		"SELECT splitByChar('=', Tag1)[1] AS value FROM graphite_tagged  WHERE "+
			"Date >= '"+fromDate+"' AND Date <= '"+untilDate+"' GROUP BY value ORDER BY value LIMIT 10000",
		&chtest.TestResponse{
			Body: []byte("__name__\nenvironment\nproject\nhost\n"),
		})

	// Test 2: Get tags with prefix filter
	srv.AddResponce(
		"SELECT splitByChar('=', Tag1)[1] AS value FROM graphite_tagged  WHERE "+
			"(Tag1 LIKE 'pr%') AND (Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 10000",
		&chtest.TestResponse{
			Body: []byte("project\n"),
		})

	// Test 3: Get tags with expr filters
	srv.AddResponce(
		"SELECT splitByChar('=', arrayJoin(Tags))[1] AS value FROM graphite_tagged  WHERE "+
			"(Tag1='environment=production') AND (Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 10001",
		&chtest.TestResponse{
			Body: []byte("__name__\nhost\nproject\n"),
		})

	// Test 4: Get tags with multiple expr filters
	srv.AddResponce(
		"SELECT splitByChar('=', arrayJoin(Tags))[1] AS value FROM graphite_tagged  WHERE "+
			"((Tag1='environment=production') AND (has(Tags, 'project=web'))) AND (Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 10002",
		&chtest.TestResponse{
			Body: []byte("__name__\nhost\n"),
		})

	// Test 5: Get tags with prefix and expr filters
	srv.AddResponce(
		"SELECT splitByChar('=', arrayJoin(Tags))[1] AS value FROM graphite_tagged  WHERE "+
			"((Tag1='environment=production') AND (arrayJoin(Tags) LIKE 'h%')) AND (Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 10001",
		&chtest.TestResponse{
			Body: []byte("host\n"),
		})

	tests := []testStruct{
		{
			request:  NewRequest("GET", srv.URL+"/tags/autoComplete/tags", nil),
			wantCode: http.StatusOK,
			want:     `["environment","host","name","project"]`,
		},
		{
			request:  NewRequest("GET", srv.URL+"/tags/autoComplete/tags?tagPrefix=pr", nil),
			wantCode: http.StatusOK,
			want:     `["project"]`,
		},
		{
			request:  NewRequest("GET", srv.URL+"/tags/autoComplete/tags?expr=environment%3Dproduction", nil),
			wantCode: http.StatusOK,
			want:     `["host","name","project"]`,
		},
		{
			request:  NewRequest("GET", srv.URL+"/tags/autoComplete/tags?expr=environment%3Dproduction&expr=project%3Dweb", nil),
			wantCode: http.StatusOK,
			want:     `["host","name"]`,
		},
		{
			request:  NewRequest("GET", srv.URL+"/tags/autoComplete/tags?expr=environment%3Dproduction&tagPrefix=h", nil),
			wantCode: http.StatusOK,
			want:     `["host"]`,
		},
	}

	for i, tt := range tests {
		t.Run("Test#"+strconv.Itoa(i), func(t *testing.T) {
			testResponce(t, i, h, &tt, "")
		})
	}
}

func TestHandler_ServeTagsWithCache(t *testing.T) {
	timeNow = func() time.Time {
		return time.Unix(1669714247, 0)
	}

	metrics.DisableMetrics()

	srv := chtest.NewTestServer()
	defer srv.Close()

	cfg, _ := config.DefaultConfig()
	cfg.ClickHouse.URL = srv.URL
	cfg.ClickHouse.TaggedTable = "graphite_tagged"

	// Enable cache
	cfg.Common.FindCacheConfig = config.CacheConfig{
		Type:           "mem",
		Size:           8192,
		FindTimeoutSec: 1,
	}

	var err error

	cfg.Common.FindCache, err = config.CreateCache("autocomplete", &cfg.Common.FindCacheConfig)
	if err != nil {
		t.Fatalf("Failed to create find cache: %v", err)
	}

	h := NewTags(cfg)

	now := timeNow()
	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, now)

	srv.AddResponce(
		"SELECT splitByChar('=', Tag1)[1] AS value FROM graphite_tagged  WHERE "+
			"Date >= '"+fromDate+"' AND Date <= '"+untilDate+"' GROUP BY value ORDER BY value LIMIT 10000",
		&chtest.TestResponse{
			Body: []byte("__name__\nenvironment\nproject\nhost\n"),
		})

	test := testStruct{
		request:  NewRequest("GET", srv.URL+"/tags/autoComplete/tags", nil),
		wantCode: http.StatusOK,
		want:     `["environment","host","name","project"]`,
	}

	// First request - should hit the database
	testResponce(t, 0, h, &test, "")
	assert.Equal(t, uint64(1), srv.Queries())

	// Second request - should hit the cache
	testResponce(t, 1, h, &test, "1")
	assert.Equal(t, uint64(1), srv.Queries()) // No new queries

	// Wait for cache expiration
	time.Sleep(time.Second * 2)

	// Third request - should hit the database again
	testResponce(t, 2, h, &test, "")
	assert.Equal(t, uint64(2), srv.Queries())
}

func TestHandler_ServeValues(t *testing.T) {
	timeNow = func() time.Time {
		return time.Unix(1669714247, 0)
	}

	metrics.DisableMetrics()

	srv := chtest.NewTestServer()
	defer srv.Close()

	cfg, _ := config.DefaultConfig()
	cfg.ClickHouse.URL = srv.URL
	cfg.ClickHouse.TaggedTable = "graphite_tagged"

	h := NewValues(cfg)

	now := timeNow()
	until := strconv.FormatInt(now.Unix(), 10)
	from := strconv.FormatInt(now.Add(-time.Minute).Unix(), 10)
	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, now)

	srv.AddResponce(
		"SELECT substr(arrayFilter(x -> x LIKE 'host=%', Tags)[1], 6) AS value FROM graphite_tagged  WHERE (((Tag1='environment=production') AND (has(Tags, 'project=web'))) AND (arrayExists(x -> x LIKE 'host=%', Tags))) AND "+
			"(Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 10000",
		&chtest.TestResponse{
			Body: []byte("host1\nhost2\ndc-host2\ndc-host3\n"),
		})

	tests := []testStruct{
		{
			request: NewRequest("GET", srv.URL+"/tags/autoComplete/values?"+
				"expr=environment%3Dproduction"+"&"+"expr=project%3Dweb"+"&"+"tag=host"+
				"&limit=10000&from="+from+"&until="+until, nil),
			wantCode:    http.StatusOK,
			want:        "[\"host1\",\"host2\",\"dc-host2\",\"dc-host3\"]",
			wantContent: "text/plain; charset=utf-8",
		},
	}

	var queries uint64

	for i, tt := range tests {
		t.Run(tt.request.URL.RawQuery+"#"+strconv.Itoa(i), func(t *testing.T) {
			for i := 0; i < 2; i++ {
				testResponce(t, i, h, &tt, "")
			}

			assert.Equal(t, uint64(2), srv.Queries()-queries)
			queries = srv.Queries()
		})
	}
}

func TestHandler_ServeValuesWithValuePrefix(t *testing.T) {
	timeNow = func() time.Time {
		return time.Unix(1669714247, 0)
	}

	metrics.DisableMetrics()

	srv := chtest.NewTestServer()
	defer srv.Close()

	cfg, _ := config.DefaultConfig()
	cfg.ClickHouse.URL = srv.URL
	cfg.ClickHouse.TaggedTable = "graphite_tagged"

	h := NewValues(cfg)

	now := timeNow()
	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, now)

	// Test with valuePrefix
	srv.AddResponce(
		"SELECT substr(Tag1, 6) AS value FROM graphite_tagged  WHERE "+
			"(Tag1 LIKE 'host=dc-%') AND (Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 100",
		&chtest.TestResponse{
			Body: []byte("dc-host1\ndc-host2\ndc-host3\n"),
		})

	test := testStruct{
		request:  NewRequest("GET", srv.URL+"/tags/autoComplete/values?tag=host&valuePrefix=dc-&limit=100", nil),
		wantCode: http.StatusOK,
		want:     "[\"dc-host1\",\"dc-host2\",\"dc-host3\"]",
	}

	testResponce(t, 0, h, &test, "")
}

func TestHandler_ServeValuesNameTag(t *testing.T) {
	timeNow = func() time.Time {
		return time.Unix(1669714247, 0)
	}

	metrics.DisableMetrics()

	srv := chtest.NewTestServer()
	defer srv.Close()

	cfg, _ := config.DefaultConfig()
	cfg.ClickHouse.URL = srv.URL
	cfg.ClickHouse.TaggedTable = "graphite_tagged"

	h := NewValues(cfg)

	now := timeNow()
	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, now)

	// Test with name tag (which should be converted to __name__)
	srv.AddResponce(
		`SELECT substr(Tag1, 10) AS value FROM graphite_tagged  WHERE `+
			`(Tag1 LIKE '\\_\\_name\\_\\_=metric.%') AND (Date >= '`+fromDate+`' AND Date <= '`+untilDate+`') GROUP BY value ORDER BY value LIMIT 10000`,
		&chtest.TestResponse{
			Body: []byte("metric.cpu.usage\nmetric.memory.used\nmetric.disk.io\n"),
		})

	test := testStruct{
		request:  NewRequest("GET", srv.URL+"/tags/autoComplete/values?tag=name&valuePrefix=metric.", nil),
		wantCode: http.StatusOK,
		want:     "[\"metric.cpu.usage\",\"metric.memory.used\",\"metric.disk.io\"]",
	}

	testResponce(t, 0, h, &test, "")
}

func TestHandler_ServeValuesWithCache(t *testing.T) {
	timeNow = func() time.Time {
		return time.Unix(1669714247, 0)
	}

	metrics.DisableMetrics()

	srv := chtest.NewTestServer()
	defer srv.Close()

	cfg, _ := config.DefaultConfig()
	cfg.ClickHouse.URL = srv.URL
	cfg.ClickHouse.TaggedTable = "graphite_tagged"

	// Enable cache
	cfg.Common.FindCacheConfig = config.CacheConfig{
		Type:           "mem",
		Size:           8192,
		FindTimeoutSec: 1,
	}

	var err error

	cfg.Common.FindCache, err = config.CreateCache("autocomplete", &cfg.Common.FindCacheConfig)
	if err != nil {
		t.Fatalf("Failed to create find cache: %v", err)
	}

	h := NewValues(cfg)

	now := timeNow()
	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, now)

	srv.AddResponce(
		"SELECT substr(Tag1, 6) AS value FROM graphite_tagged  WHERE "+
			"(Tag1 LIKE 'host=%') AND (Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 10000",
		&chtest.TestResponse{
			Body: []byte("host1\nhost2\nhost3\n"),
		})

	test := testStruct{
		request:  NewRequest("GET", srv.URL+"/tags/autoComplete/values?tag=host", nil),
		wantCode: http.StatusOK,
		want:     "[\"host1\",\"host2\",\"host3\"]",
	}

	// First request - should hit the database
	testResponce(t, 0, h, &test, "")
	assert.Equal(t, uint64(1), srv.Queries())

	// Second request - should hit the cache
	testResponce(t, 1, h, &test, "1")
	assert.Equal(t, uint64(1), srv.Queries()) // No new queries

	// Wait for cache expiration
	time.Sleep(time.Second * 2)

	// Third request - should hit the database again
	testResponce(t, 2, h, &test, "")
	assert.Equal(t, uint64(2), srv.Queries())
}

func TestHandler_ServeValuesWithCacheAndExpr(t *testing.T) {
	timeNow = func() time.Time {
		return time.Unix(1669714247, 0)
	}

	metrics.DisableMetrics()

	srv := chtest.NewTestServer()
	defer srv.Close()

	cfg, _ := config.DefaultConfig()
	cfg.ClickHouse.URL = srv.URL
	cfg.ClickHouse.TaggedTable = "graphite_tagged"

	// Enable cache
	cfg.Common.FindCacheConfig = config.CacheConfig{
		Type:           "mem",
		Size:           8192,
		FindTimeoutSec: 1,
	}

	var err error

	cfg.Common.FindCache, err = config.CreateCache("autocomplete", &cfg.Common.FindCacheConfig)
	if err != nil {
		t.Fatalf("Failed to create find cache: %v", err)
	}

	h := NewValues(cfg)

	now := timeNow()
	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, now)

	srv.AddResponce(
		"SELECT substr(arrayFilter(x -> x LIKE 'host=%', Tags)[1], 6) AS value FROM graphite_tagged  WHERE "+
			"((Tag1='environment=production') AND (arrayExists(x -> x LIKE 'host=%', Tags))) AND (Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 10000",
		&chtest.TestResponse{
			Body: []byte("prod-host1\nprod-host2\n"),
		})

	test := testStruct{
		request:  NewRequest("GET", srv.URL+"/tags/autoComplete/values?tag=host&expr=environment%3Dproduction", nil),
		wantCode: http.StatusOK,
		want:     "[\"prod-host1\",\"prod-host2\"]",
	}

	// First request - should hit the database
	testResponce(t, 0, h, &test, "")
	assert.Equal(t, uint64(1), srv.Queries())

	// Second request - should hit the cache
	testResponce(t, 1, h, &test, "1")
	assert.Equal(t, uint64(1), srv.Queries()) // No new queries

	// Test with different valuePrefix - should not hit cache
	test2 := testStruct{
		request:  NewRequest("GET", srv.URL+"/tags/autoComplete/values?tag=host&expr=environment%3Dproduction&valuePrefix=prod-host1", nil),
		wantCode: http.StatusOK,
		want:     "[\"prod-host1\"]",
	}

	srv.AddResponce(
		"SELECT substr(arrayFilter(x -> x LIKE 'host=prod-host1%', Tags)[1], 6) AS value FROM graphite_tagged  WHERE "+
			"((Tag1='environment=production') AND (arrayExists(x -> x LIKE 'host=prod-host1%', Tags))) AND (Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 10000",
		&chtest.TestResponse{
			Body: []byte("prod-host1\n"),
		})

	// Should hit the database because valuePrefix is different
	testResponce(t, 2, h, &test2, "")
	assert.Equal(t, uint64(2), srv.Queries())
}

func TestHandler_ServeValuesWithInvalidLimit(t *testing.T) {
	timeNow = func() time.Time {
		return time.Unix(1669714247, 0)
	}

	metrics.DisableMetrics()

	srv := chtest.NewTestServer()
	defer srv.Close()

	cfg, _ := config.DefaultConfig()
	cfg.ClickHouse.URL = srv.URL
	cfg.ClickHouse.TaggedTable = "graphite_tagged"

	h := NewValues(cfg)

	test := testStruct{
		request:  NewRequest("GET", srv.URL+"/tags/autoComplete/values?tag=host&limit=invalid", nil),
		wantCode: http.StatusBadRequest,
		want:     "", // Error response
	}

	testResponce(t, 0, h, &test, "")
}

func TestHandler_ServeValuesWithMultipleExpr(t *testing.T) {
	timeNow = func() time.Time {
		return time.Unix(1669714247, 0)
	}

	metrics.DisableMetrics()

	srv := chtest.NewTestServer()
	defer srv.Close()

	cfg, _ := config.DefaultConfig()
	cfg.ClickHouse.URL = srv.URL
	cfg.ClickHouse.TaggedTable = "graphite_tagged"

	h := NewValues(cfg)

	now := timeNow()
	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, now)

	// Test with multiple expressions and valuePrefix
	srv.AddResponce(
		"SELECT substr(arrayFilter(x -> x LIKE 'host=dc-%', Tags)[1], 6) AS value FROM graphite_tagged  WHERE "+
			"(((Tag1='environment=production') AND (has(Tags, 'project=web'))) AND (arrayExists(x -> x LIKE 'host=dc-%', Tags))) AND "+
			"(Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 10000",
		&chtest.TestResponse{
			Body: []byte("dc-host1\ndc-host2\n"),
		})

	test := testStruct{
		request:  NewRequest("GET", srv.URL+"/tags/autoComplete/values?tag=host&expr=environment%3Dproduction&expr=project%3Dweb&valuePrefix=dc-", nil),
		wantCode: http.StatusOK,
		want:     "[\"dc-host1\",\"dc-host2\"]",
	}

	testResponce(t, 0, h, &test, "")
}

func TestHandler_ServeValuesNoCache(t *testing.T) {
	timeNow = func() time.Time {
		return time.Unix(1669714247, 0)
	}

	metrics.DisableMetrics()

	srv := chtest.NewTestServer()
	defer srv.Close()

	cfg, _ := config.DefaultConfig()
	cfg.ClickHouse.URL = srv.URL
	cfg.ClickHouse.TaggedTable = "graphite_tagged"

	// Enable cache
	cfg.Common.FindCacheConfig = config.CacheConfig{
		Type:           "mem",
		Size:           8192,
		FindTimeoutSec: 60,
	}

	var err error

	cfg.Common.FindCache, err = config.CreateCache("autocomplete", &cfg.Common.FindCacheConfig)
	if err != nil {
		t.Fatalf("Failed to create find cache: %v", err)
	}

	h := NewValues(cfg)

	now := timeNow()
	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, now)

	srv.AddResponce(
		"SELECT substr(Tag1, 6) AS value FROM graphite_tagged  WHERE "+
			"(Tag1 LIKE 'host=%') AND (Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 10000",
		&chtest.TestResponse{
			Body: []byte("host1\nhost2\n"),
		})

	test := testStruct{
		request:  NewRequest("GET", srv.URL+"/tags/autoComplete/values?tag=host&noCache=true", nil),
		wantCode: http.StatusOK,
		want:     "[\"host1\",\"host2\"]",
	}

	// First request with noCache=true - should always hit the database
	testResponce(t, 0, h, &test, "")
	assert.Equal(t, uint64(1), srv.Queries())

	// Second request with noCache=true - should hit the database again
	testResponce(t, 1, h, &test, "")
	assert.Equal(t, uint64(2), srv.Queries()) // Should increase
}

func TestHandler_ServeValuesEmptyResult(t *testing.T) {
	timeNow = func() time.Time {
		return time.Unix(1669714247, 0)
	}

	metrics.DisableMetrics()

	srv := chtest.NewTestServer()
	defer srv.Close()

	cfg, _ := config.DefaultConfig()
	cfg.ClickHouse.URL = srv.URL
	cfg.ClickHouse.TaggedTable = "graphite_tagged"

	h := NewValues(cfg)

	now := timeNow()
	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, now)

	// Test empty result
	srv.AddResponce(
		"SELECT substr(Tag1, 13) AS value FROM graphite_tagged  WHERE "+
			"(Tag1 LIKE 'nonexistent=%') AND (Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 10000",
		&chtest.TestResponse{
			Body: []byte(""), // Empty response
		})

	test := testStruct{
		request:  NewRequest("GET", srv.URL+"/tags/autoComplete/values?tag=nonexistent", nil),
		wantCode: http.StatusOK,
		want:     "null", // Empty array
	}

	testResponce(t, 0, h, &test, "")
}

func TestHandler_ServeTagsWithCostOptimization(t *testing.T) {
	timeNow = func() time.Time {
		return time.Unix(1669714247, 0)
	}

	metrics.DisableMetrics()

	srv := chtest.NewTestServer()
	defer srv.Close()

	cfg, _ := config.DefaultConfig()
	cfg.ClickHouse.URL = srv.URL
	cfg.ClickHouse.TaggedTable = "graphite_tagged"
	cfg.ClickHouse.TagsCountTable = "tag1_count_per_day"

	h := NewTags(cfg)

	now := timeNow()
	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, now)
	from := now.AddDate(0, 0, -h.config.ClickHouse.TaggedAutocompleDays).Unix()
	until := now.Unix()

	// Test case: Tags query with multiple expressions should use cost optimization
	// First response: tags count query to get costs
	srv.AddResponce(
		"SELECT Tag1, sum(Count) as cnt FROM tag1_count_per_day WHERE "+
			"((Tag1='environment=production') OR (Tag1='project=web')) AND "+
			"(Date >= '"+date.FromTimestampToDaysFormat(from)+"' AND Date <= '"+date.UntilTimestampToDaysFormat(until)+"') GROUP BY Tag1 FORMAT TabSeparatedRaw",
		&chtest.TestResponse{
			Body: []byte("environment=production\t10000\nproject=web\t500\n"),
		})

	// Second response: main tags query (should be ordered based on costs)
	srv.AddResponce(
		"SELECT splitByChar('=', arrayJoin(Tags))[1] AS value FROM graphite_tagged  WHERE "+
			"((Tag1='project=web') AND (has(Tags, 'environment=production'))) AND "+ // Lower cost term first
			"(Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 10002",
		&chtest.TestResponse{
			Body: []byte("__name__\nhost\nregion\n"),
		})

	test := testStruct{
		request:  NewRequest("GET", srv.URL+"/tags/autoComplete/tags?expr=environment%3Dproduction&expr=project%3Dweb", nil),
		wantCode: http.StatusOK,
		want:     `["host","name","region"]`,
	}

	testResponce(t, 0, h, &test, "")
	assert.Equal(t, uint64(2), srv.Queries()) // Should have 2 queries: cost query + main query
}

func TestHandler_ServeValuesWithCostOptimization(t *testing.T) {
	timeNow = func() time.Time {
		return time.Unix(1669714247, 0)
	}

	metrics.DisableMetrics()

	srv := chtest.NewTestServer()
	defer srv.Close()

	cfg, _ := config.DefaultConfig()
	cfg.ClickHouse.URL = srv.URL
	cfg.ClickHouse.TaggedTable = "graphite_tagged"
	cfg.ClickHouse.TagsCountTable = "tag1_count_per_day"

	h := NewValues(cfg)

	now := timeNow()
	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, now)
	from := now.AddDate(0, 0, -h.config.ClickHouse.TaggedAutocompleDays).Unix()
	until := now.Unix()

	// First response: tags count query for cost optimization
	srv.AddResponce(
		"SELECT Tag1, sum(Count) as cnt FROM tag1_count_per_day WHERE "+
			"((Tag1='environment=production') OR (Tag1='datacenter=us-east')) AND "+
			"(Date >= '"+date.FromTimestampToDaysFormat(from)+"' AND Date <= '"+date.UntilTimestampToDaysFormat(until)+"') GROUP BY Tag1 FORMAT TabSeparatedRaw",
		&chtest.TestResponse{
			Body: []byte("environment=production\t5000\ndatacenter=us-east\t100\n"),
		})

	// Second response: values query (should use optimized order)
	srv.AddResponce(
		"SELECT substr(arrayFilter(x -> x LIKE 'host=%', Tags)[1], 6) AS value FROM graphite_tagged  WHERE "+
			"(((Tag1='datacenter=us-east') AND (has(Tags, 'environment=production'))) AND "+ // Lower cost first
			"(arrayExists(x -> x LIKE 'host=%', Tags))) AND "+
			"(Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 10000",
		&chtest.TestResponse{
			Body: []byte("host1\nhost2\nhost3\n"),
		})

	test := testStruct{
		request:  NewRequest("GET", srv.URL+"/tags/autoComplete/values?tag=host&expr=environment%3Dproduction&expr=datacenter%3Dus-east", nil),
		wantCode: http.StatusOK,
		want:     "[\"host1\",\"host2\",\"host3\"]",
	}

	testResponce(t, 0, h, &test, "")
	assert.Equal(t, uint64(2), srv.Queries())
}

func TestHandler_ServeTagsWithWildcardExpressions(t *testing.T) {
	timeNow = func() time.Time {
		return time.Unix(1669714247, 0)
	}

	metrics.DisableMetrics()

	srv := chtest.NewTestServer()
	defer srv.Close()

	cfg, _ := config.DefaultConfig()
	cfg.ClickHouse.URL = srv.URL
	cfg.ClickHouse.TaggedTable = "graphite_tagged"
	cfg.ClickHouse.TagsCountTable = "tag1_count_per_day"

	h := NewTags(cfg)

	now := timeNow()
	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, now)

	// Test with wildcard expressions (should not query tags count table)
	srv.AddResponce(
		"SELECT splitByChar('=', arrayJoin(Tags))[1] AS value FROM graphite_tagged  WHERE "+
			"(Tag1 LIKE 'environment=prod%') AND (Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 10001",
		&chtest.TestResponse{
			Body: []byte("__name__\nhost\nproject\n"),
		})

	test := testStruct{
		request:  NewRequest("GET", srv.URL+"/tags/autoComplete/tags?expr=environment%3Dprod*", nil),
		wantCode: http.StatusOK,
		want:     `["host","name","project"]`,
	}

	testResponce(t, 0, h, &test, "")
	assert.Equal(t, uint64(1), srv.Queries()) // Only 1 query since wildcards skip cost optimization
}

func TestHandler_ServeValuesWithNoEqualityTerms(t *testing.T) {
	timeNow = func() time.Time {
		return time.Unix(1669714247, 0)
	}

	metrics.DisableMetrics()

	srv := chtest.NewTestServer()
	defer srv.Close()

	cfg, _ := config.DefaultConfig()
	cfg.ClickHouse.URL = srv.URL
	cfg.ClickHouse.TaggedTable = "graphite_tagged"
	cfg.ClickHouse.TagsCountTable = "tag1_count_per_day"

	h := NewValues(cfg)

	now := timeNow()
	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, now)

	// Test with != operator (should not use tags count table)
	srv.AddResponce(
		"SELECT substr(arrayFilter(x -> x LIKE 'host=%', Tags)[1], 6) AS value FROM graphite_tagged  WHERE "+
			"((NOT arrayExists((x) -> x='environment=development', Tags)) AND (arrayExists(x -> x LIKE 'host=%', Tags))) AND "+
			"(Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 10000",
		&chtest.TestResponse{
			Body: []byte("host1\nhost2\n"),
		})

	test := testStruct{
		request:  NewRequest("GET", srv.URL+"/tags/autoComplete/values?tag=host&expr=environment%21%3Ddevelopment", nil),
		wantCode: http.StatusOK,
		want:     "[\"host1\",\"host2\"]",
	}

	testResponce(t, 0, h, &test, "")
	assert.Equal(t, uint64(1), srv.Queries()) // Only 1 query since no equality terms
}

func TestHandler_ServeTagsWithHighCostTags(t *testing.T) {
	timeNow = func() time.Time {
		return time.Unix(1669714247, 0)
	}

	metrics.DisableMetrics()

	srv := chtest.NewTestServer()
	defer srv.Close()

	cfg, _ := config.DefaultConfig()
	cfg.ClickHouse.URL = srv.URL
	cfg.ClickHouse.TaggedTable = "graphite_tagged"
	cfg.ClickHouse.TagsCountTable = "tag1_count_per_day"

	h := NewTags(cfg)

	now := timeNow()
	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, now)
	from := now.AddDate(0, 0, -h.config.ClickHouse.TaggedAutocompleDays).Unix()
	until := now.Unix()

	// Test with high cardinality tags - tags should be reordered based on cost
	srv.AddResponce(
		"SELECT Tag1, sum(Count) as cnt FROM tag1_count_per_day WHERE "+
			"(((Tag1='__name__=high.cost.metric') OR (Tag1='environment=production')) OR (Tag1='dc=west')) AND "+
			"(Date >= '"+date.FromTimestampToDaysFormat(from)+"' AND Date <= '"+date.UntilTimestampToDaysFormat(until)+"') GROUP BY Tag1 FORMAT TabSeparatedRaw",
		&chtest.TestResponse{
			Body: []byte("__name__=high.cost.metric\t1000000\nenvironment=production\t10000\ndc=west\t50\n"),
		})

	// Query should use lowest cost tag first (dc=west)
	srv.AddResponce(
		"SELECT splitByChar('=', arrayJoin(Tags))[1] AS value FROM graphite_tagged  WHERE "+
			"(((Tag1='dc=west') AND (has(Tags, 'environment=production'))) AND (has(Tags, '__name__=high.cost.metric'))) AND "+
			"(Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 10003",
		&chtest.TestResponse{
			Body: []byte("host\nproject\nregion\n"),
		})

	test := testStruct{
		request:  NewRequest("GET", srv.URL+"/tags/autoComplete/tags?expr=name%3Dhigh.cost.metric&expr=environment%3Dproduction&expr=dc%3Dwest", nil),
		wantCode: http.StatusOK,
		want:     `["host","project","region"]`,
	}

	testResponce(t, 0, h, &test, "")
	assert.Equal(t, uint64(2), srv.Queries())
}

func TestHandler_ServeValuesWithMixedOperators(t *testing.T) {
	timeNow = func() time.Time {
		return time.Unix(1669714247, 0)
	}

	metrics.DisableMetrics()

	srv := chtest.NewTestServer()
	defer srv.Close()

	cfg, _ := config.DefaultConfig()
	cfg.ClickHouse.URL = srv.URL
	cfg.ClickHouse.TaggedTable = "graphite_tagged"
	cfg.ClickHouse.TagsCountTable = "tag1_count_per_day"

	h := NewValues(cfg)

	now := timeNow()
	fromDate, untilDate := dateString(h.config.ClickHouse.TaggedAutocompleDays, now)
	from := now.AddDate(0, 0, -h.config.ClickHouse.TaggedAutocompleDays).Unix()
	until := now.Unix()

	// Test with mixed operators (only equality operators should be in cost query)
	srv.AddResponce(
		"SELECT Tag1, sum(Count) as cnt FROM tag1_count_per_day WHERE "+
			"((Tag1='environment=production') OR (Tag1='project=api')) AND "+
			"(Date >= '"+date.FromTimestampToDaysFormat(from)+"' AND Date <= '"+date.UntilTimestampToDaysFormat(until)+"') GROUP BY Tag1 FORMAT TabSeparatedRaw",
		&chtest.TestResponse{
			Body: []byte("environment=production\t8000\nproject=api\t200\n"),
		})

	// Main query should include != operator but order by cost
	srv.AddResponce(
		"SELECT substr(arrayFilter(x -> x LIKE 'host=%', Tags)[1], 6) AS value FROM graphite_tagged  WHERE "+
			"((((Tag1='project=api') AND (has(Tags, 'environment=production'))) AND (NOT arrayExists((x) -> x='dc=east', Tags))) AND "+
			"(arrayExists(x -> x LIKE 'host=%', Tags))) AND "+
			"(Date >= '"+fromDate+"' AND Date <= '"+untilDate+"') GROUP BY value ORDER BY value LIMIT 10000",
		&chtest.TestResponse{
			Body: []byte("host1\nhost2\n"),
		})

	test := testStruct{
		request:  NewRequest("GET", srv.URL+"/tags/autoComplete/values?tag=host&expr=environment%3Dproduction&expr=project%3Dapi&expr=dc%21%3Deast", nil),
		wantCode: http.StatusOK,
		want:     "[\"host1\",\"host2\"]",
	}

	testResponce(t, 0, h, &test, "")
	assert.Equal(t, uint64(2), srv.Queries()) // Cost query only for equality terms
}


================================================
FILE: cache/cache.go
================================================
package cache

import (
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"sync/atomic"
	"time"

	"github.com/bradfitz/gomemcache/memcache"

	"github.com/msaf1980/go-expirecache"
)

var (
	ErrTimeout  = errors.New("cache: timeout")
	ErrNotFound = errors.New("cache: not found")
)

type BytesCache interface {
	Get(k string) ([]byte, error)
	Set(k string, v []byte, expire int32)
}

func NewExpireCache(maxsize uint64) BytesCache {
	ec := expirecache.New[string, []byte](maxsize)
	go ec.ApproximateCleaner(10 * time.Second)

	return &ExpireCache{ec: ec}
}

type ExpireCache struct {
	ec *expirecache.Cache[string, []byte]
}

func (ec ExpireCache) Get(k string) ([]byte, error) {
	v, ok := ec.ec.Get(k)

	if !ok {
		return nil, ErrNotFound
	}

	return v, nil
}

func (ec ExpireCache) Set(k string, v []byte, expire int32) {
	ec.ec.Set(k, v, uint64(len(v)), expire)
}

func NewMemcached(prefix string, servers ...string) BytesCache {
	return &MemcachedCache{prefix: prefix, client: memcache.New(servers...)}
}

type MemcachedCache struct {
	prefix   string
	client   *memcache.Client
	timeouts uint64
}

func (m *MemcachedCache) Get(k string) ([]byte, error) {
	key := sha256.Sum256([]byte(k))
	hk := hex.EncodeToString(key[:])
	done := make(chan bool, 1)

	var err error

	var item *memcache.Item

	go func() {
		item, err = m.client.Get(m.prefix + hk)
		done <- true
	}()

	timeout := time.After(50 * time.Millisecond)

	select {
	case <-timeout:
		atomic.AddUint64(&m.timeouts, 1)
		return nil, ErrTimeout
	case <-done:
	}

	if err != nil {
		// translate to internal cache miss error
		if errors.Is(err, memcache.ErrCacheMiss) {
			err = ErrNotFound
		}

		return nil, err
	}

	return item.Value, nil
}

func (m *MemcachedCache) Set(k string, v []byte, expire int32) {
	key := sha256.Sum256([]byte(k))
	hk := hex.EncodeToString(key[:])

	go func() {
		_ = m.client.Set(&memcache.Item{Key: m.prefix + hk, Value: v, Expiration: expire})
	}()
}

func (m *MemcachedCache) Timeouts() uint64 {
	return atomic.LoadUint64(&m.timeouts)
}


================================================
FILE: capabilities/handler.go
================================================
package capabilities

import (
	"encoding/json"
	"io"
	"net/http"
	"os"
	"time"

	v3pb "github.com/go-graphite/protocol/carbonapi_v3_pb"
	"github.com/lomik/graphite-clickhouse/config"
	"github.com/lomik/graphite-clickhouse/logs"
	"github.com/lomik/graphite-clickhouse/pkg/scope"
)

type Handler struct {
	config *config.Config
}

func NewHandler(config *config.Config) *Handler {
	return &Handler{
		config: config,
	}
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	accessLogger := scope.LoggerWithHeaders(r.Context(), r, h.config.Common.HeadersToLog).Named("http")
	logger := scope.LoggerWithHeaders(r.Context(), r, h.config.Common.HeadersToLog).Named("capabilities")

	r = r.WithContext(scope.WithLogger(r.Context(), logger))

	r.ParseMultipartForm(1024 * 1024)

	format := r.FormValue("format")

	accepts := r.Header["Accept"]
	for _, accept := range accepts {
		if accept == "application/x-carbonapi-v3-pb" {
			format = "carbonapi_v3_pb"
			break
		}
	}

	status := http.StatusOK
	start := time.Now()

	defer func() {
		d := time.Since(start)
		logs.AccessLog(accessLogger, h.config, r, status, d, time.Duration(0), false, false)
	}()

	if format == "carbonapi_v3_pb" || format == "json" {
		body, err := io.ReadAll(r.Body)
		if err != nil {
			status = http.StatusBadRequest
			http.Error(w, "Bad request (malformed body)", status)
		}

		var pv3Request v3pb.CapabilityRequest

		err = pv3Request.Unmarshal(body)
		if err != nil {
			status = http.StatusBadRequest
			http.Error(w, "Bad request (malformed body)", status)
		}

		hostname, err := os.Hostname()
		if err != nil {
			hostname = "(unknown)"
		}

		pvResponse := v3pb.CapabilityResponse{
			SupportedProtocols:        []string{"carbonapi_v3_pb", "carbonapi_v2_pb", "graphite-web-pickle"},
			Name:                      hostname,
			HighPrecisionTimestamps:   false,
			SupportFilteringFunctions: false,
			LikeSplittedRequests:      false,
			SupportStreaming:          false,
		}

		var data []byte

		contentType := ""

		switch format {
		case "json":
			contentType = "application/json"

			data, err = json.Marshal(pvResponse)
			if err != nil {
				status = http.StatusInternalServerError
				http.Error(w, err.Error(), status)

				return
			}
		case "carbonapi_v3_pb":
			contentType = "application/x-carbonapi-v3-pb"

			data, err = pvResponse.Marshal()
			if err != nil {
				status = http.StatusBadRequest
				http.Error(w, "Bad request (unsupported format)", status)

				return
			}
		}

		w.Header().Set("Content-Type", contentType)
		w.Write(data)
	} else {
		status = http.StatusBadRequest
		http.Error(w, "Bad request (unsupported format)", status)
	}
}


================================================
FILE: cmd/e2e-test/carbon-clickhouse.go
================================================
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strings"
	"text/template"
)

var CchContainerName = "carbon-clickhouse-gch-test"

type CarbonClickhouse struct {
	Version string `toml:"version"`

	DockerImage string `toml:"image"`

	Template string `toml:"template"` // carbon-clickhouse config template

	TZ string `toml:"tz"` // override timezone

	address   string `toml:"-"`
	container string `toml:"-"`
	storeDir  string `toml:"-"`
}

func (c *CarbonClickhouse) Start(testDir, clickhouseURL string) (string, error) {
	if len(c.Version) == 0 {
		c.Version = "latest"
	}

	if len(c.DockerImage) == 0 {
		c.DockerImage = "ghcr.io/go-graphite/carbon-clickhouse"
	}

	var err error

	c.address, err = getFreeTCPPort("")
	if err != nil {
		return "", err
	}

	c.container = CchContainerName

	c.storeDir, err = os.MkdirTemp("", "carbon-clickhouse")
	if err != nil {
		return "", err
	}

	c.address, err = getFreeTCPPort("")
	if err != nil {
		c.Cleanup()
		return "", err
	}

	name := filepath.Base(c.Template)
	tpl := path.Join(testDir, c.Template)

	tmpl, err := template.New(name).ParseFiles(tpl)
	if err != nil {
		c.Cleanup()
		return "", err
	}

	param := struct {
		CLICKHOUSE_URL string
		CCH_ADDR       string
	}{
		CLICKHOUSE_URL: clickhouseURL,
		CCH_ADDR:       c.address,
	}

	configFile := path.Join(c.storeDir, "carbon-clickhouse.conf")

	f, err := os.OpenFile(configFile, os.O_WRONLY|os.O_CREATE, 0644)
	if err != nil {
		c.Cleanup()
		return "", err
	}

	err = tmpl.ExecuteTemplate(f, name, param)
	if err != nil {
		c.Cleanup()
		return "", err
	}

	tz := os.Getenv("TZ")

	cchStart := []string{"run", "-d",
		"--name", c.container,
		"-p", c.address + ":2003",
		"-v", c.storeDir + ":/etc/carbon-clickhouse",
		// TZ, need to be same as graphite-clickhouse for prevent bugs, ike issue #184
		"-v", "/etc/timezone:/etc/timezone:ro",
		"-v", "/etc/localtime:/etc/localtime:ro",
		"-e", "TZ=" + tz,
		"--network", DockerNetwork,
	}

	cchStart = append(cchStart, c.DockerImage+":"+c.Version)

	cmd := exec.Command(DockerBinary, cchStart...)

	out, err := cmd.CombinedOutput()
	if err == nil {
		dateLocal, _ := exec.Command("date").Output()
		dateLocalStr := strings.TrimRight(string(dateLocal), "\n")
		_, dateOnCCH := containerExec(c.container, []string{"date"})
		fmt.Printf("date local %s, on carbon-clickhouse %s\n", dateLocalStr, dateOnCCH)
	}

	return string(out), err
}

func (c *CarbonClickhouse) Stop(delete bool) (string, error) {
	if len(c.container) == 0 {
		return "", nil
	}

	chStop := []string{"stop", c.container}

	cmd := exec.Command(DockerBinary, chStop...)
	out, err := cmd.CombinedOutput()

	if err == nil && delete {
		return c.Delete()
	}

	return string(out), err
}

func (c *CarbonClickhouse) Delete() (string, error) {
	if len(c.container) == 0 {
		return "", nil
	}

	chDel := []string{"rm", c.container}

	cmd := exec.Command(DockerBinary, chDel...)
	out, err := cmd.CombinedOutput()

	if err == nil {
		c.container = ""
	}

	c.Cleanup()

	return string(out), err
}

func (c *CarbonClickhouse) Cleanup() {
	if c.storeDir != "" {
		os.RemoveAll(c.storeDir)
		c.storeDir = ""
	}
}

func (c *CarbonClickhouse) Address() string {
	return c.address
}

func (c *CarbonClickhouse) Container() string {
	return c.container
}


================================================
FILE: cmd/e2e-test/checks.go
================================================
package main

import (
	"bufio"
	"fmt"
	"net/http"
	"os"
	"reflect"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/go-graphite/protocol/carbonapi_v3_pb"
	"github.com/lomik/graphite-clickhouse/helper/client"
	"github.com/lomik/graphite-clickhouse/helper/datetime"
	"github.com/lomik/graphite-clickhouse/helper/tests/compare"
)

func isFindCached(header http.Header) (string, bool) {
	if header == nil {
		return "", false
	}

	v, exist := header["X-Cached-Find"]
	if len(v) == 0 {
		return "", false
	}

	return v[0], exist
}

func requestId(header http.Header) string {
	if header == nil {
		return ""
	}

	v, exist := header["X-Gch-Request-Id"]
	if exist && len(v) > 0 {
		return v[0]
	}

	return ""
}

func compareFindMatch(errors *[]string, name, url string, actual, expected []client.FindMatch, findCached bool, cacheTTL int, header http.Header) {
	var cacheTTLStr string
	if findCached {
		cacheTTLStr = strconv.Itoa(cacheTTL)
	}

	id := requestId(header)

	if header != nil {
		v, actualFindCached := isFindCached(header)
		if actualFindCached != findCached || cacheTTLStr != v {
			*errors = append(*errors, fmt.Sprintf("TRY[%s] %s %s: X-Cached-Find want '%s', got '%s'", name, id, url, cacheTTLStr, v))
		}
	}

	maxLen := compare.Max(len(expected), len(actual))
	for i := 0; i < maxLen; i++ {
		if i > len(actual)-1 {
			*errors = append(*errors, fmt.Sprintf("- TRY[%s] %s %s [%d] = %+v", name, id, url, i, expected[i]))
		} else if i > len(expected)-1 {
			*errors = append(*errors, fmt.Sprintf("+ TRY[%s] %s %s [%d] = %+v", name, id, url, i, actual[i]))
		} else if expected[i] != actual[i] {
			*errors = append(*errors, fmt.Sprintf("- TRY[%s] %s %s [%d] = %+v", name, id, url, i, expected[i]))
			*errors = append(*errors, fmt.Sprintf("+ TRY[%s] %s %s [%d] = %+v", name, id, url, i, actual[i]))
		}
	}
}

func verifyMetricsFind(ch *Clickhouse, gch *GraphiteClickhouse, check *MetricsFindCheck) []string {
	var errors []string

	httpClient := http.Client{
		Timeout: check.Timeout,
	}
	address := gch.URL()

	for _, format := range check.Formats {
		name := ""

		if url, result, respHeader, err := client.MetricsFind(&httpClient, address, format, check.Query, check.from, check.until); err == nil {
			id := requestId(respHeader)
			if check.ErrorRegexp != "" {
				errors = append(errors, fmt.Sprintf("TRY[%s] %s %s: want error with '%s'", "", id, url, check.ErrorRegexp))
			}

			compareFindMatch(&errors, name, url, result, check.Result, check.InCache, check.CacheTTL, respHeader)

			if len(result) == 0 && len(check.Result) > 0 {
				gch.Grep(id)

				if len(check.DumpIfEmpty) > 0 {
					for _, q := range check.DumpIfEmpty {
						if out, err := ch.Query(q); err == nil {
							fmt.Fprintf(os.Stderr, "%s\n%s", q, out)
						} else {
							fmt.Fprintf(os.Stderr, "%s: %s\n", err.Error(), q)
						}
					}
				}
			}

			if check.CacheTTL > 0 && check.ErrorRegexp == "" {
				// second query must be find-cached
				name = "cache"
				if url, result, respHeader, err = client.MetricsFind(&httpClient, address, format, check.Query, check.from, check.until); err == nil {
					compareFindMatch(&errors, name, url, result, check.Result, true, check.CacheTTL, respHeader)
				} else {
					errStr := strings.TrimRight(err.Error(), "\n")
					errors = append(errors, fmt.Sprintf("TRY[%s] %s %s: %s", name, requestId(respHeader), url, errStr))
				}
			}
		} else {
			errStr := strings.TrimRight(err.Error(), "\n")
			if check.errorRegexp == nil || !check.errorRegexp.MatchString(errStr) {
				errors = append(errors, fmt.Sprintf("TRY[%s] %s %s: want error with '%s', got '%s'", "", requestId(respHeader), url, check.ErrorRegexp, errStr))
			} else {
				fmt.Printf("EXPECTED ERROR, SUCCESS %s : %s\n", url, errStr)
			}
		}
	}

	return errors
}

func compareTags(errors *[]string, name, url string, actual, expected []string, findCached bool, cacheTTL int, header http.Header) {
	var cacheTTLStr string
	if findCached {
		cacheTTLStr = strconv.Itoa(cacheTTL)
	}

	id := requestId(header)

	if header != nil {
		v, actualFindCached := isFindCached(header)
		if actualFindCached != findCached || cacheTTLStr != v {
			*errors = append(*errors, fmt.Sprintf("TRY[%s] %s %s: X-Cached-Find want '%s', got '%s'", name, id, url, cacheTTLStr, v))
		}
	}

	maxLen := compare.Max(len(expected), len(actual))
	for i := 0; i < maxLen; i++ {
		if i > len(actual)-1 {
			*errors = append(*errors, fmt.Sprintf("- TRY[%s] %s %s [%d] = %+v", name, id, url, i, expected[i]))
		} else if i > len(expected)-1 {
			*errors = append(*errors, fmt.Sprintf("+ TRY[%s] %s %s [%d] = %+v", name, id, url, i, actual[i]))
		} else if expected[i] != actual[i] {
			*errors = append(*errors, fmt.Sprintf("- TRY[%s] %s %s [%d] = %+v", name, id, url, i, expected[i]))
			*errors = append(*errors, fmt.Sprintf("+ TRY[%s] %s %s [%d] = %+v", name, id, url, i, actual[i]))
		}
	}
}

func verifyTags(ch *Clickhouse, gch *GraphiteClickhouse, check *TagsCheck) []string {
	var errors []string

	httpClient := http.Client{
		Timeout: check.Timeout,
	}
	address := gch.URL()

	for _, format := range check.Formats {
		var (
			result     []string
			err        error
			url        string
			respHeader http.Header
		)

		name := ""

		if check.Names {
			url, result, respHeader, err = client.TagsNames(&httpClient, address, format, check.Query, check.Limits, check.from, check.until)
		} else {
			url, result, respHeader, err = client.TagsValues(&httpClient, address, format, check.Query, check.Limits, check.from, check.until)
		}

		if err == nil {
			id := requestId(respHeader)
			if check.ErrorRegexp != "" {
				errors = append(errors, fmt.Sprintf("TRY[%s] %s %s: want error with '%s'", "", id, url, check.ErrorRegexp))
			}

			compareTags(&errors, name, url, result, check.Result, check.InCache, check.CacheTTL, respHeader)

			if len(result) == 0 && len(check.Result) > 0 {
				gch.Grep(id)

				if len(check.DumpIfEmpty) > 0 {
					for _, q := range check.DumpIfEmpty {
						if out, err := ch.Query(q); err == nil {
							fmt.Fprintf(os.Stderr, "%s\n%s", q, out)
						} else {
							fmt.Fprintf(os.Stderr, "%s: %s\n", err.Error(), q)
						}
					}
				}
			}

			if check.CacheTTL > 0 && check.ErrorRegexp == "" {
				// second query must be find-cached
				name = "cache"

				if check.Names {
					url, result, respHeader, err = client.TagsNames(&httpClient, address, format, check.Query, check.Limits, check.from, check.until)
				} else {
					url, result, respHeader, err = client.TagsValues(&httpClient, address, format, check.Query, check.Limits, check.from, check.until)
				}

				if err == nil {
					compareTags(&errors, name, url, result, check.Result, true, check.CacheTTL, respHeader)
				} else {
					errStr := strings.TrimRight(err.Error(), "\n")
					errors = append(errors, fmt.Sprintf("TRY[%s] %s %s: %s", name, requestId(respHeader), url, errStr))
				}
			}
		} else {
			errStr := strings.TrimRight(err.Error(), "\n")
			if check.errorRegexp == nil || !check.errorRegexp.MatchString(errStr) {
				errors = append(errors, fmt.Sprintf("TRY[%s] %s %s: want error with '%s', got '%s'", "", requestId(respHeader), url, check.ErrorRegexp, errStr))
			} else {
				fmt.Printf("EXPECTED ERROR, SUCCESS %s : %s\n", url, errStr)
			}
		}
	}

	return errors
}

func compareRender(errors *[]string, name, url string, actual, expected []client.Metric, findCached bool, header http.Header, cacheTTL int) {
	var cacheTTLStr string
	if findCached {
		cacheTTLStr = strconv.Itoa(cacheTTL)
	}

	sort.Slice(actual, func(i, j int) bool {
		return actual[i].Name < actual[j].Name
	})

	id := requestId(header)

	if header != nil {
		v, actualFindCached := isFindCached(header)
		if actualFindCached != findCached || cacheTTLStr != v {
			*errors = append(*errors, fmt.Sprintf("TRY[%s] %s %s: X-Cached-Find want '%s', got '%s'", name, id, url, cacheTTLStr, v))
		}
	}

	maxLen := compare.Max(len(expected), len(actual))
	for i := 0; i < maxLen; i++ {
		if i > len(actual)-1 {
			*errors = append(*errors, fmt.Sprintf("- TRY[%s] %s %s [%d] = %+v", name, id, url, i, expected[i]))
		} else if i > len(expected)-1 {
			*errors = append(*errors, fmt.Sprintf("+ TRY[%s] %s %s [%d] = %+v", name, id, url, i, actual[i]))
		} else if actual[i].Name != expected[i].Name {
			*errors = append(*errors, fmt.Sprintf("- TRY[%s] %s %s [%d] = %+v", name, id, url, i, expected[i]))
			*errors = append(*errors, fmt.Sprintf("+ TRY[%s] %s %s [%d] = %+v", name, id, url, i, actual[i]))
		} else {
			if actual[i].PathExpression != expected[i].PathExpression {
				*errors = append(*errors, fmt.Sprintf("TRY[%s] %s %s '%s': mismatch [%d].PathExpression, got '%s', want '%s'", name, id, url, actual[i].Name, i, actual[i].PathExpression, expected[i].PathExpression))
			}

			if actual[i].ConsolidationFunc != expected[i].ConsolidationFunc {
				*errors = append(*errors, fmt.Sprintf("TRY[%s] %s %s '%s': mismatch [%d].ConsolidationFunc, got '%s', want '%s'", name, id, url, actual[i].Name, i, actual[i].ConsolidationFunc, expected[i].ConsolidationFunc))
			}

			if actual[i].ConsolidationFunc != expected[i].ConsolidationFunc {
				*errors = append(*errors, fmt.Sprintf("TRY[%s] %s %s '%s': mismatch [%d].ConsolidationFunc, got '%s', want '%s'", name, id, url, actual[i].Name, i, actual[i].ConsolidationFunc, expected[i].ConsolidationFunc))
			}

			if actual[i].StartTime != expected[i].StartTime {
				*errors = append(*errors, fmt.Sprintf("TRY[%s] %s %s '%s': mismatch [%d].StartTime, got %d, want %d", name, id, url, actual[i].Name, i, actual[i].StartTime, expected[i].StartTime))
			}

			if actual[i].StopTime != expected[i].StopTime {
				*errors = append(*errors, fmt.Sprintf("TRY[%s] %s %s '%s': mismatch [%d].StopTime, got %d, want %d", name, id, url, actual[i].Name, i, actual[i].StopTime, expected[i].StopTime))
			}

			if actual[i].StepTime != expected[i].StepTime {
				*errors = append(*errors, fmt.Sprintf("TRY[%s] %s %s '%s': mismatch [%d].StepTime, got %d, want %d", name, id, url, actual[i].Name, i, actual[i].StepTime, expected[i].StepTime))
			}

			if actual[i].RequestStartTime != expected[i].RequestStartTime {
				*errors = append(*errors, fmt.Sprintf("TRY[%s] %s %s '%s': mismatch [%d].RequestStartTime, got %d, want %d", name, id, url, actual[i].Name, i, actual[i].RequestStartTime, expected[i].RequestStartTime))
			}

			if actual[i].RequestStopTime != expected[i].RequestStopTime {
				*errors = append(*errors, fmt.Sprintf("TRY[%s] %s %s '%s': mismatch [%d].RequestStopTime, got %d, want %d", name, id, url, actual[i].Name, i, actual[i].RequestStopTime, expected[i].RequestStopTime))
			}

			if actual[i].HighPrecisionTimestamps != expected[i].HighPrecisionTimestamps {
				*errors = append(*errors, fmt.Sprintf("TRY[%s] %s %s '%s': mismatch [%d].HighPrecisionTimestamps, got %v, want %v", name, id, url, actual[i].Name, i, actual[i].HighPrecisionTimestamps, expected[i].HighPrecisionTimestamps))
			}

			if !reflect.DeepEqual(actual[i].AppliedFunctions, expected[i].AppliedFunctions) {
				*errors = append(*errors, fmt.Sprintf("TRY[%s] %s %s '%s': mismatch [%d].AppliedFunctions, got '%s', want '%s'", name, id, url, actual[i].Name, i, actual[i].AppliedFunctions, expected[i].AppliedFunctions))
			}

			if !compare.NearlyEqual(float64(actual[i].XFilesFactor), float64(expected[i].XFilesFactor)) {
				*errors = append(*errors, fmt.Sprintf("TRY[%s] %s %s '%s': mismatch [%d].XFilesFactor, got %g, want %g", name, id, url, actual[i].Name, i, actual[i].XFilesFactor, expected[i].XFilesFactor))
			}

			if !compare.NearlyEqualSlice(actual[i].Values, expected[i].Values) {
				*errors = append(*errors, fmt.Sprintf("TRY[%s] %s %s '%s': mismatch [%d].Values, got %g, want %g", name, id, url, actual[i].Name, i, actual[i].Values, expected[i].Values))
			}
		}
	}
}

func parseFilteringFunctions(strFilteringFuncs []string) ([]*carbonapi_v3_pb.FilteringFunction, error) {
	res := make([]*carbonapi_v3_pb.FilteringFunction, 0, len(strFilteringFuncs))

	for _, strFF := range strFilteringFuncs {
		strFFSplit := strings.Split(strFF, "(")
		if len(strFFSplit) != 2 {
			return nil, fmt.Errorf("could not parse filtering function: %s", strFF)
		}

		name := strFFSplit[0]

		args := strings.Split(strFFSplit[1], ",")
		for i := range args {
			args[i] = strings.TrimSpace(args[i])
			args[i] = strings.Trim(args[i], ")'")
		}

		res = append(res, &carbonapi_v3_pb.FilteringFunction{Name: name, Arguments: args})
	}

	return res, nil
}

func verifyRender(ch *Clickhouse, gch *GraphiteClickhouse, check *RenderCheck, defaultPreision time.Duration) []string {
	var errors []string

	httpClient := http.Client{
		Timeout: check.Timeout,
	}
	address := gch.URL()
	from := datetime.TimestampTruncate(check.from, defaultPreision)
	until := datetime.TimestampTruncate(check.until, defaultPreision)

	for _, format := range check.Formats {
		var filteringFunctions []*carbonapi_v3_pb.FilteringFunction

		if format == client.FormatPb_v3 {
			var err error

			filteringFunctions, err = parseFilteringFunctions(check.FilteringFunctions)
			if err != nil {
				errors = append(errors, err.Error())
				continue
			}
		}

		if url, result, respHeader, err := client.Render(&httpClient, address, format, check.Targets, filteringFunctions, check.MaxDataPoints, from, until); err == nil {
			id := requestId(respHeader)
			name := ""

			if check.ErrorRegexp != "" {
				errors = append(errors, fmt.Sprintf("TRY[%s] %s %s: want error with '%s'", "", id, url, check.ErrorRegexp))
			}

			compareRender(&errors, name, url, result, check.result, check.InCache, respHeader, check.CacheTTL)

			if len(result) == 0 && len(check.result) > 0 {
				gch.Grep(id)

				if len(check.DumpIfEmpty) > 0 {
					for _, q := range check.DumpIfEmpty {
						if out, err := ch.Query(q); err == nil {
							fmt.Fprintf(os.Stderr, "%s\n%s", q, out)
						} else {
							fmt.Fprintf(os.Stderr, "%s: %s\n", err.Error(), q)
						}
					}
				}
			}

			if check.CacheTTL > 0 && check.ErrorRegexp == "" {
				// second query must be find-cached
				name = "cache"
				if url, result, respHeader, err = client.Render(&httpClient, address, format, check.Targets, filteringFunctions, check.MaxDataPoints, from, until); err == nil {
					compareRender(&errors, name, url, result, check.result, true, respHeader, check.CacheTTL)
				} else {
					errStr := strings.TrimRight(err.Error(), "\n")
					errors = append(errors, fmt.Sprintf("TRY[%s] %s %s: %s", name, requestId(respHeader), url, errStr))
				}
			}
		} else {
			errStr := strings.TrimRight(err.Error(), "\n")
			if check.errorRegexp == nil || !check.errorRegexp.MatchString(errStr) {
				errors = append(errors, fmt.Sprintf("TRY[%s] %s %s: want error with '%s', got '%s'", "", requestId(respHeader), url, check.ErrorRegexp, errStr))
			} else {
				fmt.Printf("EXPECTED ERROR, SUCCESS %s : %s\n", url, errStr)
			}
		}
	}

	return errors
}

func debug(test *TestSchema, ch *Clickhouse, gch *GraphiteClickhouse) {
	for {
		cmd := gch.Cmd()
		fmt.Println(cmd)
		fmt.Printf("graphite-clickhouse URL: %s , clickhouse URL: %s , proxy URL: %s (delay %v)\n",
			gch.URL(), ch.URL(), test.Proxy.URL(), test.Proxy.GetDelay())
		fmt.Printf("graphite-clickhouse log: %s , clickhouse container: %s\n",
			gch.storeDir+"/graphite-clickhouse.log", ch.container)
		fmt.Println("Some queries was failed, press y for continue after debug test, k for kill graphite-clickhouse:")

		in := bufio.NewScanner(os.Stdin)
		in.Scan()

		s := in.Text()
		if s == "y" || s == "Y" {
			break
		} else if s == "k" || s == "K" {
			gch.Stop(false)
		}
	}
}


================================================
FILE: cmd/e2e-test/clickhouse.go
================================================
package main

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"strconv"
	"strings"

	"github.com/msaf1980/go-stringutils"
)

var (
	ClickhouseContainerName = "clickhouse-server-gch-test"
	ClickhouseOldImage      = "yandex/clickhouse-server"
	ClickhouseDefaultImage  = "clickhouse/clickhouse-server"
)

type Clickhouse struct {
	Version    string `toml:"version"`
	Dir        string `toml:"dir"`
	TLSEnabled bool   `toml:"tls"`

	DockerImage string `toml:"image"`

	TZ string `toml:"tz"` // override timezone

	httpAddress  string `toml:"-"`
	httpsAddress string `toml:"-"`
	url          string `toml:"-"`
	tlsurl       string `toml:"-"`
	container    string `toml:"-"`
}

func (c *Clickhouse) CheckConfig(rootDir string) error {
	if c.Version == "" {
		c.Version = "latest"
	}

	if len(c.Dir) == 0 {
		return ErrNoSetDir
	}

	if !strings.HasPrefix(c.Dir, "/") {
		c.Dir = rootDir + "/" + c.Dir
	}

	if c.DockerImage == "" {
		if c.Version == "latest" {
			c.DockerImage = ClickhouseDefaultImage
		} else {
			splitV := strings.Split(c.Version, ".")

			majorV, err := strconv.Atoi(splitV[0])
			if err != nil {
				c.DockerImage = ClickhouseDefaultImage
			} else if majorV >= 21 {
				c.DockerImage = ClickhouseDefaultImage
			} else {
				c.DockerImage = ClickhouseOldImage
			}
		}
	}

	return nil
}

func (c *Clickhouse) Key() string {
	return c.DockerImage + ":" + c.Version + " " + c.Dir + " TZ " + c.TZ
}

func (c *Clickhouse) Start() (string, error) {
	var err error

	c.httpAddress, err = getFreeTCPPort("")
	if err != nil {
		return "", err
	}

	port := strings.Split(c.httpAddress, ":")[1]
	c.url = "http://" + c.httpAddress

	c.container = ClickhouseContainerName

	// tz, _ := localTZLocationName()

	chStart := []string{"run", "-d",
		"--name", c.container,
		"--ulimit", "nofile=262144:262144",
		"-p", port + ":8123",
		// "-e", "TZ=" + tz, // workaround for TZ=":/etc/localtime"
		"-v", c.Dir + "/config.xml:/etc/clickhouse-server/config.xml",
		"-v", c.Dir + "/users.xml:/etc/clickhouse-server/users.xml",
		"-v", c.Dir + "/rollup.xml:/etc/clickhouse-server/config.d/rollup.xml",
		"-v", c.Dir + "/init.sql:/docker-entrypoint-initdb.d/init.sql",
		"--network", DockerNetwork,
	}

	if c.TLSEnabled {
		c.httpsAddress, err = getFreeTCPPort("")
		if err != nil {
			return "", err
		}

		port = strings.Split(c.httpsAddress, ":")[1]
		c.tlsurl = "https://" + c.httpsAddress
		chStart = append(chStart,
			"-v", c.Dir+"/server.crt:/etc/clickhouse-server/server.crt",
			"-v", c.Dir+"/server.key:/etc/clickhouse-server/server.key",
			"-v", c.Dir+"/rootCA.crt:/etc/clickhouse-server/rootCA.crt",
			"-p", port+":8443",
		)
	}

	if c.TZ != "" {
		chStart = append(chStart, "-e", "TZ="+c.TZ)
	}

	chStart = append(chStart, c.DockerImage+":"+c.Version)

	cmd := exec.Command(DockerBinary, chStart...)
	out, err := cmd.CombinedOutput()

	return string(out), err
}

func (c *Clickhouse) Stop(delete bool) (string, error) {
	if len(c.container) == 0 {
		return "", nil
	}

	chStop := []string{"stop", c.container}

	cmd := exec.Command(DockerBinary, chStop...)
	out, err := cmd.CombinedOutput()

	if err == nil && delete {
		return c.Delete()
	}

	return string(out), err
}

func (c *Clickhouse) Delete() (string, error) {
	if len(c.container) == 0 {
		return "", nil
	}

	chDel := []string{"rm", c.container}

	cmd := exec.Command(DockerBinary, chDel...)
	out, err := cmd.CombinedOutput()

	if err == nil {
		c.container = ""
	}

	return string(out), err
}

func (c *Clickhouse) URL() string {
	return c.url
}

func (c *Clickhouse) TLSURL() string {
	return c.tlsurl
}

func (c *Clickhouse) Container() string {
	return c.container
}

func (c *Clickhouse) Exec(sql string) (bool, string) {
	return containerExec(c.container, []string{"sh", "-c", "clickhouse-client -q '" + sql + "'"})
}

func (c *Clickhouse) Query(sql string) (string, error) {
	reader := strings.NewReader(sql)

	request, err := http.NewRequest(http.MethodPost, c.URL(), reader)
	if err != nil {
		return "", err
	}

	resp, err := http.DefaultClient.Do(request)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	msg, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	if resp.StatusCode != http.StatusOK {
		return "", errors.New(resp.Status + ": " + string(bytes.TrimRight(msg, "\n")))
	}

	return string(msg), nil
}

func (c *Clickhouse) Alive() bool {
	if len(c.container) == 0 {
		return false
	}

	req, err := http.DefaultClient.Get(c.URL())
	if err != nil {
		return false
	}

	defer req.Body.Close()

	return req.StatusCode == http.StatusOK
}

func (c *Clickhouse) CopyLog(destDir string, tail uint64) error {
	if len(c.container) == 0 {
		return nil
	}

	dest := destDir + "/clickhouse-server.log"

	chArgs := []string{"cp", c.container + ":/var/log/clickhouse-server/clickhouse-server.log", dest}

	cmd := exec.Command(DockerBinary, chArgs...)

	out, err := cmd.CombinedOutput()
	if err != nil {
		return errors.New(err.Error() + ": " + string(bytes.TrimRight(out, "\n")))
	}

	if tail > 0 {
		out, _ := exec.Command("tail", "-"+strconv.FormatUint(tail, 10), dest).Output()
		fmt.Fprintf(os.Stderr, "CLICKHOUSE-SERVER.LOG %s", stringutils.UnsafeString(out))
	}

	return nil
}

func (c *Clickhouse) CopyErrLog(destDir string, tail uint64) error {
	if len(c.container) == 0 {
		return nil
	}

	dest := destDir + "/clickhouse-server.err.log"

	chArgs := []string{"cp", c.container + ":/var/log/clickhouse-server/clickhouse-server.err.log", dest}

	cmd := exec.Command(DockerBinary, chArgs...)

	out, err := cmd.CombinedOutput()
	if err != nil {
		return errors.New(err.Error() + ": " + string(bytes.TrimRight(out, "\n")))
	}

	if tail > 0 {
		out, _ := exec.Command("tail", "-"+strconv.FormatUint(tail, 10), dest).Output()
		fmt.Fprintf(os.Stderr, "CLICKHOUSE-SERVER.ERR %s", stringutils.UnsafeString(out))
	}

	return nil
}


================================================
FILE: cmd/e2e-test/container.go
================================================
package main

import (
	"os/exec"
	"strings"
)

var (
	DockerBinary  string
	DockerNetwork string = "graphite-ch-test"
)

func imageDelete(image, version string) (bool, string) {
	if len(DockerBinary) == 0 {
		panic("docker not set")
	}

	chArgs := []string{"rmi", image + ":" + version}

	cmd := exec.Command(DockerBinary, chArgs...)
	out, err := cmd.CombinedOutput()
	s := strings.Trim(string(out), "\n")

	if err == nil {
		return true, s
	}

	return false, err.Error() + ": " + s
}

func containerExist(name string) (bool, string) {
	if len(DockerBinary) == 0 {
		panic("docker not set")
	}

	chInspect := []string{"inspect", "--format", "'{{.Name}}'", name}

	cmd := exec.Command(DockerBinary, chInspect...)
	out, err := cmd.CombinedOutput()
	s := strings.Trim(string(out), "\n")

	if err == nil {
		return true, s
	}

	return false, err.Error() + ": " + s
}

func containerRemove(name string) (bool, string) {
	if len(DockerBinary) == 0 {
		panic("docker not set")
	}

	chInspect := []string{"rm", "-f", name}

	cmd := exec.Command(DockerBinary, chInspect...)
	out, err := cmd.CombinedOutput()
	s := strings.Trim(string(out), "\n")

	if err == nil {
		return true, s
	}

	return false, err.Error() + ": " + s
}

func containerExec(name string, args []string) (bool, string) {
	if len(DockerBinary) == 0 {
		panic("docker not set")
	}

	dCmd := []string{"exec", name}
	dCmd = append(dCmd, args...)

	cmd := exec.Command(DockerBinary, dCmd...)
	out, err := cmd.CombinedOutput()
	s := strings.Trim(string(out), "\n")

	if err == nil {
		return true, s
	}

	return false, err.Error() + ": " + s
}


================================================
FILE: cmd/e2e-test/e2etesting.go
================================================
package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"path"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"

	"go.uber.org/zap"

	"github.com/lomik/graphite-clickhouse/helper/client"
	"github.com/lomik/graphite-clickhouse/helper/datetime"

	"github.com/pelletier/go-toml"
)

var (
	preSQL = []string{
		"TRUNCATE TABLE IF EXISTS graphite_reverse",
		"TRUNCATE TABLE IF EXISTS graphite",
		"TRUNCATE TABLE IF EXISTS graphite_index",
		"TRUNCATE TABLE IF EXISTS graphite_tags",
	}
)

type Point struct {
	Value float64       `toml:"value"`
	Time  string        `toml:"time"`
	Delay time.Duration `toml:"delay"`

	time int64 `toml:"-"`
}

type InputMetric struct {
	Name   string        `toml:"name"`
	Points []Point       `toml:"points"`
	Round  time.Duration `toml:"round"`
}

type Metric struct {
	Name                    string    `toml:"name"`
	PathExpression          string    `toml:"path"`
	ConsolidationFunc       string    `toml:"consolidation"`
	StartTime               string    `toml:"start"`
	StopTime                string    `toml:"stop"`
	StepTime                int64     `toml:"step"`
	XFilesFactor            float32   `toml:"xfiles"`
	HighPrecisionTimestamps bool      `toml:"high_precision"`
	Values                  []float64 `toml:"values"`
	AppliedFunctions        []string  `toml:"applied_functions"`
	RequestStartTime        string    `toml:"req_start"`
	RequestStopTime         string    `toml:"req_stop"`
}

type RenderCheck struct {
	Name               string              `toml:"name"`
	Formats            []client.FormatType `toml:"formats"`
	From               string              `toml:"from"`
	Until              string              `toml:"until"`
	Targets            []string            `toml:"targets"`
	MaxDataPoints      int64               `toml:"max_data_points"`
	FilteringFunctions []string            `toml:"filtering_functions"`
	Timeout            time.Duration       `toml:"timeout"`
	DumpIfEmpty        []string            `toml:"dump_if_empty"`

	Optimize []string `toml:"optimize"` // optimize tables before run tests

	InCache  bool `toml:"in_cache"` // already in cache
	CacheTTL int  `toml:"cache_ttl"`

	ProxyDelay         time.Duration `toml:"proxy_delay"`
	ProxyBreakWithCode int           `toml:"proxy_break_with_code"`

	Result      []Metric `toml:"result"`
	ErrorRegexp string   `toml:"error_regexp"`

	from        int64           `toml:"-"`
	until       int64           `toml:"-"`
	errorRegexp *regexp.Regexp  `toml:"-"`
	result      []client.Metric `toml:"-"`
}

type MetricsFindCheck struct {
	Name    string              `toml:"name"`
	Formats []client.FormatType `toml:"formats"`
	From    string              `toml:"from"`
	Until   string              `toml:"until"`
	Query   string              `toml:"query"`
	Timeout time.Duration       `toml:"timeout"`

	DumpIfEmpty []string `toml:"dump_if_empty"`

	InCache  bool `toml:"in_cache"` // already in cache
	CacheTTL int  `toml:"cache_ttl"`

	ProxyDelay         time.Duration `toml:"proxy_delay"`
	ProxyBreakWithCode int           `toml:"proxy_break_with_code"`

	Result      []client.FindMatch `toml:"result"`
	ErrorRegexp string             `toml:"error_regexp"`

	from        int64          `toml:"-"`
	until       int64          `toml:"-"`
	errorRegexp *regexp.Regexp `toml:"-"`
}

type TagsCheck struct {
	Name    string              `toml:"name"`
	Names   bool                `toml:"names"` // TagNames or TagValues
	Formats []client.FormatType `toml:"formats"`
	From    string              `toml:"from"`
	Until   string              `toml:"until"`
	Query   string              `toml:"query"`
	Limits  uint64              `toml:"limits"`
	Timeout time.Duration       `toml:"timeout"`

	DumpIfEmpty []string `toml:"dump_if_empty"`

	InCache  bool `toml:"in_cache"` // already in cache
	CacheTTL int  `toml:"cache_ttl"`

	ProxyDelay         time.Duration `toml:"proxy_delay"`
	ProxyBreakWithCode int           `toml:"proxy_break_with_code"`

	Result      []string `toml:"result"`
	ErrorRegexp string   `toml:"error_regexp"`

	from        int64          `toml:"-"`
	until       int64          `toml:"-"`
	errorRegexp *regexp.Regexp `toml:"-"`
}

type TestSchema struct {
	Input      []InputMetric        `toml:"input"` // carbon-clickhouse input
	Clickhouse []Clickhouse         `toml:"clickhouse"`
	Proxy      HttpReverseProxy     `toml:"clickhouse_proxy"`
	Cch        CarbonClickhouse     `toml:"carbon_clickhouse"`
	Gch        []GraphiteClickhouse `toml:"graphite_clickhouse"`

	FindChecks   []*MetricsFindCheck `toml:"find_checks"`
	TagsChecks   []*TagsCheck        `toml:"tags_checks"`
	RenderChecks []*RenderCheck      `toml:"render_checks"`

	Precision time.Duration `toml:"precision"`

	dir        string          `toml:"-"`
	name       string          `toml:"-"` // test alias (from config name)
	chVersions map[string]bool `toml:"-"`
	// input map[string][]Point `toml:"-"`
}

func (schema *TestSchema) HasTLSSettings() bool {
	return strings.Contains(schema.dir, "tls")
}

func getFreeTCPPort(name string) (string, error) {
	if len(name) == 0 {
		name = "127.0.0.1:0"
	} else if !strings.Contains(name, ":") {
		name = name + ":0"
	}

	addr, err := net.ResolveTCPAddr("tcp", name)
	if err != nil {
		return name, err
	}

	l, err := net.ListenTCP("tcp", addr)
	if err != nil {
		return name, err
	}

	defer l.Close()

	return l.Addr().String(), nil
}

func sendPlain(network, address string, metrics []InputMetric) error {
	if conn, err := net.DialTimeout(network, address, time.Second); err != nil {
		return err
	} else {
		bw := bufio.NewWriter(conn)

		for _, m := range metrics {
			conn.SetDeadline(time.Now().Add(time.Second))

			for _, point := range m.Points {
				if _, err = fmt.Fprintf(bw, "%s %f %d\n", m.Name, point.Value, point.time); err != nil {
					conn.Close()
					return err
				}

				if point.Delay > 0 {
					if err = bw.Flush(); err != nil {
						conn.Close()
						return err
					}

					time.Sleep(point.Delay)
				}
			}
		}

		if err = bw.Flush(); err != nil {
			conn.Close()
			return err
		}

		return conn.Close()
	}
}

func verifyGraphiteClickhouse(test *TestSchema, gch *GraphiteClickhouse, clickhouse *Clickhouse, testDir, clickhouseDir string, verbose, breakOnError bool, logger *zap.Logger) (testSuccess bool, verifyCount, verifyFailed int) {
	testSuccess = true

	err := gch.Start(testDir, clickhouse.URL(), test.Proxy.URL(), clickhouse.TLSURL())
	if err != nil {
		logger.Error("starting graphite-clickhouse",
			zap.String("config", test.name),
			zap.String("clickhouse version", clickhouse.Version),
			zap.String("clickhouse config", clickhouseDir),
			zap.String("graphite-clickhouse config", gch.ConfigTpl),
			zap.Error(err),
		)

		testSuccess = false

		return
	}

	for i := 100; i < 1000; i += 200 {
		time.Sleep(time.Duration(i) * time.Millisecond)

		if gch.Alive() {
			break
		}
	}

	// start tests
	for n, check := range test.FindChecks {
		verifyCount++

		test.Proxy.SetDelay(check.ProxyDelay)
		test.Proxy.SetBreakStatusCode(check.ProxyBreakWithCode)

		if len(check.Formats) == 0 {
			check.Formats = []client.FormatType{client.FormatPb_v3}
		}

		if errs := verifyMetricsFind(clickhouse, gch, check); len(errs) > 0 {
			verifyFailed++

			for _, e := range errs {
				fmt.Fprintln(os.Stderr, e)
			}

			logger.Error("verify metrics find",
				zap.String("config", test.name),
				zap.String("clickhouse version", clickhouse.Version),
				zap.String("clickhouse config", clickhouseDir),
				zap.String("graphite-clickhouse config", gch.ConfigTpl),
				zap.String("query", check.Query),
				zap.String("from_raw", check.From),
				zap.String("until_raw", check.Until),
				zap.Int64("from", check.from),
				zap.Int64("until", check.until),
				zap.String("name", check.Name+"["+strconv.Itoa(n)+"]"),
			)

			if breakOnError {
				debug(test, clickhouse, gch)
			}
		} else if verbose {
			logger.Info("verify metrics find",
				zap.String("config", test.name),
				zap.String("clickhouse version", clickhouse.Version),
				zap.String("clickhouse config", clickhouseDir),
				zap.String("graphite-clickhouse config", gch.ConfigTpl),
				zap.String("query", check.Query),
				zap.String("from_raw", check.From),
				zap.String("until_raw", check.Until),
				zap.Int64("from", check.from),
				zap.Int64("until", check.until),
				zap.String("name", check.Name+"["+strconv.Itoa(n)+"]"),
			)
		}
	}

	for n, check := range test.TagsChecks {
		verifyCount++

		test.Proxy.SetDelay(check.ProxyDelay)
		test.Proxy.SetBreakStatusCode(check.ProxyBreakWithCode)

		if len(check.Formats) == 0 {
			check.Formats = []client.FormatType{client.FormatJSON}
		}

		if errs := verifyTags(clickhouse, gch, check); len(errs) > 0 {
			verifyFailed++

			for _, e := range errs {
				fmt.Fprintln(os.Stderr, e)
			}

			logger.Error("verify tags",
				zap.String("config", test.name),
				zap.String("clickhouse version", clickhouse.Version),
				zap.String("clickhouse config", clickhouseDir),
				zap.String("graphite-clickhouse config", gch.ConfigTpl),
				zap.Bool("name", check.Names),
				zap.String("query", check.Query),
				zap.String("from_raw", check.From),
				zap.String("until_raw", check.Until),
				zap.Int64("from", check.from),
				zap.Int64("until", check.until),
				zap.String("name", check.Name+"["+strconv.Itoa(n)+"]"),
			)

			if breakOnError {
				debug(test, clickhouse, gch)
			}
		} else if verbose {
			logger.Info("verify tags",
				zap.String("config", test.name),
				zap.String("clickhouse version", clickhouse.Version),
				zap.String("clickhouse config", clickhouseDir),
				zap.String("graphite-clickhouse config", gch.ConfigTpl),
				zap.Bool("name", check.Names),
				zap.String("query", check.Query),
				zap.String("from_raw", check.From),
				zap.String("until_raw", check.Until),
				zap.Int64("from", check.from),
				zap.Int64("until", check.until),
				zap.String("name", check.Name+"["+strconv.Itoa(n)+"]"),
			)
		}
	}

	for n, check := range test.RenderChecks {
		verifyCount++

		test.Proxy.SetDelay(check.ProxyDelay)
		test.Proxy.SetBreakStatusCode(check.ProxyBreakWithCode)

		if len(check.Formats) == 0 {
			check.Formats = []client.FormatType{client.FormatPb_v3}
		}

		if len(check.Optimize) > 0 {
			for _, table := range check.Optimize {
				if success, out := clickhouse.Exec("OPTIMIZE TABLE " + table + " FINAL"); !success {
					logger.Error("optimize table",
						zap.String("config", test.name),
						zap.String("clickhouse version", clickhouse.Version),
						zap.String("clickhouse config", clickhouseDir),
						zap.String("graphite-clickhouse config", gch.ConfigTpl),
						zap.Strings("targets", check.Targets),
						zap.Strings("filtering_functions", check.FilteringFunctions),
						zap.String("from_raw", check.From),
						zap.String("until_raw", check.Until),
						zap.Int64("from", check.from),
						zap.Int64("until", check.until),
						zap.String("name", check.Name+"["+strconv.Itoa(n)+"]"),
						zap.String("table", table),
						zap.String("out", out),
					)
					time.Sleep(5 * time.Second)
				}
			}
		}

		if errs := verifyRender(clickhouse, gch, check, test.Precision); len(errs) > 0 {
			verifyFailed++

			for _, e := range errs {
				fmt.Fprintln(os.Stderr, e)
			}

			logger.Error("verify render",
				zap.String("config", test.name),
				zap.String("clickhouse version", clickhouse.Version),
				zap.String("clickhouse config", clickhouseDir),
				zap.String("graphite-clickhouse config", gch.ConfigTpl),
				zap.Strings("targets", check.Targets),
				zap.Strings("filtering_functions", check.FilteringFunctions),
				zap.String("from_raw", check.From),
				zap.String("until_raw", check.Until),
				zap.Int64("from", check.from),
				zap.Int64("until", check.until),
				zap.String("name", check.Name+"["+strconv.Itoa(n)+"]"),
			)

			if breakOnError {
				debug(test, clickhouse, gch)
			}
		} else if verbose {
			logger.Info("verify render",
				zap.String("config", test.name),
				zap.String("clickhouse version", clickhouse.Version),
				zap.String("clickhouse config", clickhouseDir),
				zap.String("graphite-clickhouse config", gch.ConfigTpl),
				zap.Strings("targets", check.Targets),
				zap.Strings("filtering_functions", check.FilteringFunctions),
				zap.String("from_raw", check.From),
				zap.String("until_raw", check.Until),
				zap.Int64("from", check.from),
				zap.Int64("until", check.until),
				zap.String("name", check.Name+"["+strconv.Itoa(n)+"]"),
			)
		}
	}

	if verifyFailed > 0 {
		testSuccess = false

		logger.Error("verify",
			zap.String("config", test.name),
			zap.String("clickhouse version", clickhouse.Version),
			zap.String("clickhouse config", clickhouseDir),
			zap.String("graphite-clickhouse config", gch.ConfigTpl),
			zap.Int64("count", int64(verifyCount)),
			zap.Int64("failed", int64(verifyFailed)),
		)
	}

	err = gch.Stop(true)
	if err != nil {
		logger.Error("stoping graphite-clickhouse",
			zap.String("config", test.name),
			zap.String("gch", gch.ConfigTpl),
			zap.String("clickhouse version", clickhouse.Version),
			zap.String("clickhouse config", clickhouseDir),
			zap.Error(err),
		)

		testSuccess = false
	}

	return
}

func testGraphiteClickhouse(test *TestSchema, clickhouse *Clickhouse, testDir, rootDir string, verbose, breakOnError bool, logger *zap.Logger) (testSuccess bool, verifyCount, verifyFailed int) {
	testSuccess = true

	for _, sql := range preSQL {
		if success, out := clickhouse.Exec(sql); !success {
			logger.Error("pre-execute",
				zap.String("config", test.name),
				zap.Any("clickhouse version", clickhouse.Version),
				zap.String("clickhouse config", clickhouse.Dir),
				zap.String("sql", sql),
				zap.String("out", out),
			)

			return
		}
	}

	if err := test.Proxy.Start(clickhouse.URL()); err != nil {
		logger.Error("starting clickhouse proxy",
			zap.String("config", test.name),
			zap.Any("clickhouse version", clickhouse.Version),
			zap.String("clickhouse config", clickhouse.Dir),
			zap.Error(err),
		)

		return
	}

	out, err := test.Cch.Start(testDir, "http://"+clickhouse.Container()+":8123")
	if err != nil {
		logger.Error("starting carbon-clickhouse",
			zap.String("config", test.name),
			zap.String("clickhouse version", clickhouse.Version),
			zap.String("clickhouse config", clickhouse.Dir),
			zap.Error(err),
			zap.String("out", out),
		)

		testSuccess = false
	}

	if testSuccess {
		logger.Info("starting e2e test",
			zap.String("config", test.name),
			zap.String("clickhouse version", clickhouse.Version),
			zap.String("clickhouse config", clickhouse.Dir),
		)
		time.Sleep(200 * time.Millisecond)
		// Populate test data
		err = sendPlain("tcp", test.Cch.address, test.Input)
		if err != nil {
			logger.Error("send plain to carbon-clickhouse",
				zap.String("config", test.name),
				zap.String("clickhouse version", clickhouse.Version),
				zap.String("clickhouse config", clickhouse.Dir),
				zap.Error(err),
			)

			testSuccess = false
		}

		if testSuccess {
			time.Sleep(2 * time.Second)
		}

		if testSuccess {
			for _, gch := range test.Gch {
				stepSuccess, vCount, vFailed := verifyGraphiteClickhouse(test, &gch, clickhouse, testDir, clickhouse.Dir, verbose, breakOnError, logger)
				verifyCount += vCount
				verifyFailed += vFailed

				if !stepSuccess {
					testSuccess = false
				}
			}
		}
	}

	out, err = test.Cch.Stop(true)
	if err != nil {
		logger.Error("stoping carbon-clickhouse",
			zap.String("config", test.name),
			zap.String("clickhouse version", clickhouse.Version),
			zap.String("clickhouse config", clickhouse.Dir),
			zap.Error(err),
			zap.String("out", out),
		)

		testSuccess = false
	}

	test.Proxy.Stop()

	if testSuccess {
		logger.Info("end e2e test",
			zap.String("config", test.name),
			zap.String("status", "success"),
			zap.String("clickhouse version", clickhouse.Version),
			zap.String("clickhouse config", clickhouse.Dir),
		)
	} else {
		logger.Error("end e2e test",
			zap.String("config", test.name),
			zap.String("status", "failed"),
			zap.String("clickhouse version", clickhouse.Version),
			zap.String("clickhouse config", clickhouse.Dir),
		)
	}

	return
}

func runTest(cfg *MainConfig, clickhouse *Clickhouse, rootDir string, now time.Time, verbose, breakOnError bool, logger *zap.Logger) (failed, total, verifyCount, verifyFailed int) {
	var isRunning bool

	total++

	if exist, out := containerExist(CchContainerName); exist {
		logger.Error("carbon-clickhouse already exist",
			zap.String("container", CchContainerName),
			zap.String("out", out),
		)

		isRunning = true
	}

	if isRunning {
		failed++
		return
	}

	success, vCount, vFailed := testGraphiteClickhouse(cfg.Test, clickhouse, cfg.Test.dir, rootDir, verbose, breakOnError, logger)
	if !success {
		failed++
	}

	verifyCount += vCount
	verifyFailed += vFailed

	return
}

func clickhouseStart(clickhouse *Clickhouse, logger *zap.Logger) bool {
	out, err := clickhouse.Start()
	if err != nil {
		logger.Error("starting clickhouse",
			zap.Any("clickhouse version", clickhouse.Version),
			zap.String("clickhouse config", clickhouse.Dir),
			zap.Error(err),
			zap.String("out", out),
		)
		clickhouse.Stop(true)

		return false
	}

	return true
}

func clickhouseStop(clickhouse *Clickhouse, logger *zap.Logger) (result bool) {
	result = true

	if !clickhouse.Alive() {
		clickhouse.CopyLog(os.TempDir(), 10)

		result = false
	}

	out, err := clickhouse.Stop(true)
	if err != nil {
		logger.Error("stoping clickhouse",
			zap.String("clickhouse version", clickhouse.Version),
			zap.String("clickhouse config", clickhouse.Dir),
			zap.Error(err),
			zap.String("out", out),
		)

		result = false
	}

	return result
}

func initTest(cfg *MainConfig, rootDir string, now time.Time, verbose, breakOnError bool, logger *zap.Logger) bool {
	tz, err := datetime.Timezone("")
	if err != nil {
		fmt.Printf("can't get timezone: %s\n", err.Error())
		os.Exit(1)
	}

	// prepare
	for n, m := range cfg.Test.Input {
		for i := range m.Points {
			m.Points[i].time = datetime.DateParamToEpoch(m.Points[i].Time, tz, now, cfg.Test.Precision)
			if m.Points[i].time == 0 {
				err = ErrTimestampInvalid
			}

			if err != nil {
				logger.Error("failed to read config",
					zap.String("config", cfg.Test.name),
					zap.Error(err),
					zap.String("input", m.Name),
					zap.Int("metric", n),
					zap.Int("point", i),
					zap.String("time", m.Points[i].Time),
				)

				return false
			}
		}
	}

	for n, find := range cfg.Test.FindChecks {
		if find.Timeout == 0 {
			find.Timeout = 10 * time.Second
		}

		find.from = datetime.DateParamToEpoch(find.From, tz, now, cfg.Test.Precision)
		if find.from == 0 && find.From != "" {
			err = ErrTimestampInvalid
		}

		if err != nil {
			logger.Error("failed to read config",
				zap.String("config", cfg.Test.name),
				zap.Error(err),
				zap.String("query", find.Query),
				zap.String("from", find.From),
				zap.Int("step", n),
			)

			return false
		}

		find.until = datetime.DateParamToEpoch(find.Until, tz, now, cfg.Test.Precision)
		if find.until == 0 && find.Until != "" {
			err = ErrTimestampInvalid
		}

		if err != nil {
			logger.Error("failed to read config",
				zap.String("config", cfg.Test.name),
				zap.Error(err),
				zap.String("query", find.Query),
				zap.String("until", find.Until),
				zap.Int("step", n),
			)

			return false
		}

		if find.ErrorRegexp != "" {
			find.errorRegexp = regexp.MustCompile(find.ErrorRegexp)
		}
	}

	for n, tags := range cfg.Test.TagsChecks {
		if tags.Timeout == 0 {
			tags.Timeout = 10 * time.Second
		}

		tags.from = datetime.DateParamToEpoch(tags.From, tz, now, cfg.Test.Precision)
		if tags.from == 0 && tags.From != "" {
			err = ErrTimestampInvalid
		}

		if err != nil {
			logger.Error("failed to read config",
				zap.String("config", cfg.Test.name),
				zap.Error(err),
				zap.String("query", tags.Query),
				zap.String("from", tags.From),
				zap.Int("find", n),
			)

			return false
		}

		tags.until = datetime.DateParamToEpoch(tags.Until, tz, now, cfg.Test.Precision)
		if tags.until == 0 && tags.Until != "" {
			err = ErrTimestampInvalid
		}

		if err != nil {
			logger.Error("failed to read config",
				zap.String("config", cfg.Test.name),
				zap.Error(err),
				zap.String("query", tags.Query),
				zap.String("until", tags.Until),
				zap.Int("tags", n),
				zap.Bool("names", tags.Names),
			)

			return false
		}

		if tags.ErrorRegexp != "" {
			tags.errorRegexp = regexp.MustCompile(tags.ErrorRegexp)
		}
	}

	for n, r := range cfg.Test.RenderChecks {
		if r.Timeout == 0 {
			r.Timeout = 10 * time.Second
		}

		r.from = datetime.DateParamToEpoch(r.From, tz, now, cfg.Test.Precision)
		if r.from == 0 && r.From != "" {
			err = ErrTimestampInvalid
		}

		if err != nil {
			logger.Error("failed to read config",
				zap.String("config", cfg.Test.name),
				zap.Error(err),
				zap.Strings("targets", r.Targets),
				zap.String("from", r.From),
				zap.Int("render", n),
			)

			return false
		}

		r.until = datetime.DateParamToEpoch(r.Until, tz, now, cfg.Test.Precision)
		if r.until == 0 && r.Until != "" {
			err = ErrTimestampInvalid
		}

		if err != nil {
			logger.Error("failed to read config",
				zap.String("config", cfg.Test.name),
				zap.Error(err),
				zap.Strings("targets", r.Targets),
				zap.String("until", r.Until),
				zap.Int("render", n),
			)

			return false
		}

		if r.ErrorRegexp != "" {
			r.errorRegexp = regexp.MustCompile(r.ErrorRegexp)
		}

		sort.Slice(r.Result, func(i, j int) bool {
			return r.Result[i].Name < r.Result[j].Name
		})

		r.result = make([]client.Metric, len(r.Result))
		for i, result := range r.Result {
			r.result[i].StartTime = datetime.DateParamToEpoch(result.StartTime, tz, now, cfg.Test.Precision)
			if r.result[i].StartTime == 0 && result.StartTime != "" {
				err = ErrTimestampInvalid
			}

			if err != nil {
				logger.Error("failed to read config",
					zap.String("config", cfg.Test.name),
					zap.Error(err),
					zap.Strings("targets", r.Targets),
					zap.Int("render", n),
					zap.String("metric", result.Name),
					zap.String("start", result.StartTime),
				)

				return false
			}

			r.result[i].StopTime = datetime.DateParamToEpoch(result.StopTime, tz, now, cfg.Test.Precision)
			if r.result[i].StopTime == 0 && result.StopTime != "" {
				err = ErrTimestampInvalid
			}

			if err != nil {
				logger.Error("failed to read config",
					zap.String("config", cfg.Test.name),
					zap.Error(err),
					zap.Strings("targets", r.Targets),
					zap.Int("render", n),
					zap.String("metric", result.Name),
					zap.String("stop", result.StopTime),
				)

				return false
			}

			r.result[i].RequestStartTime = datetime.DateParamToEpoch(result.RequestStartTime, tz, now, cfg.Test.Precision)
			if r.result[i].RequestStartTime == 0 && result.RequestStartTime != "" {
				err = ErrTimestampInvalid
			}

			if err != nil {
				logger.Error("failed to read config",
					zap.String("config", cfg.Test.name),
					zap.Error(err),
					zap.Strings("targets", r.Targets),
					zap.Int("render", n),
					zap.String("metric", result.Name),
					zap.String("req_start", result.RequestStartTime),
				)

				return false
			}

			r.result[i].RequestStopTime = datetime.DateParamToEpoch(result.RequestStopTime, tz, now, cfg.Test.Precision)
			if r.result[i].RequestStopTime == 0 && result.RequestStopTime != "" {
				err = ErrTimestampInvalid
			}

			if err != nil {
				logger.Error("failed to read config",
					zap.String("config", cfg.Test.name),
					zap.Error(err),
					zap.Strings("targets", r.Targets),
					zap.Int("render", n),
					zap.String("metric", result.Name),
					zap.String("req_stop", result.RequestStopTime),
				)

				return false
			}

			r.result[i].StepTime = result.StepTime
			r.result[i].Name = result.Name
			r.result[i].PathExpression = result.PathExpression
			r.result[i].ConsolidationFunc = result.ConsolidationFunc
			r.result[i].XFilesFactor = result.XFilesFactor
			r.result[i].HighPrecisionTimestamps = result.HighPrecisionTimestamps
			r.result[i].AppliedFunctions = result.AppliedFunctions
			r.result[i].Values = result.Values
		}
	}

	return true
}

func loadConfig(config string, rootDir string) (*MainConfig, error) {
	d, err := os.ReadFile(config)
	if err != nil {
		return nil, err
	}

	confShort := strings.ReplaceAll(config, rootDir+"/", "")

	var cfg = &MainConfig{}
	if err := toml.Unmarshal(d, cfg); err != nil {
		return nil, err
	}

	cfg.Test.name = confShort
	cfg.Test.dir = path.Dir(config)

	if cfg.Test == nil {
		return nil, ErrNoTest
	}

	cfg.Test.chVersions = make(map[string]bool)
	for i := range cfg.Test.Clickhouse {
		if err := cfg.Test.Clickhouse[i].CheckConfig(rootDir); err == nil {
			cfg.Test.chVersions[cfg.Test.Clickhouse[i].Key()] = true
		} else {
			return nil, fmt.Errorf("[%d] %s", i, err.Error())
		}
	}

	return cfg, nil
}


================================================
FILE: cmd/e2e-test/errors.go
================================================
package main

import "errors"

var (
	ErrTimestampInvalid = errors.New("invalid timestamp")
	ErrNoTest           = errors.New("no test section")
	ErrNoSetDir         = errors.New("dir not set")
)


================================================
FILE: cmd/e2e-test/graphite-clickhouse.go
================================================
package main

import (
	"errors"
	"fmt"
	"net/http"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strings"
	"syscall"
	"text/template"

	"github.com/msaf1980/go-stringutils"

	"github.com/lomik/graphite-clickhouse/helper/client"
)

type GraphiteClickhouse struct {
	Binary    string `toml:"binary"`
	ConfigTpl string `toml:"template"`
	TestDir   string `toml:"-"`

	TZ string `toml:"tz"` // override timezone

	storeDir   string    `toml:"-"`
	configFile string    `toml:"-"`
	address    string    `toml:"-"`
	cmd        *exec.Cmd `toml:"-"`
}

func (c *GraphiteClickhouse) Start(testDir, chURL, chProxyURL, chTLSURL string) error {
	if c.cmd != nil {
		return errors.New("carbon-clickhouse already started")
	}

	if len(c.Binary) == 0 {
		c.Binary = "./graphite-clickhouse"
	}

	if len(c.ConfigTpl) == 0 {
		return errors.New("graphite-clickhouse config template not set")
	}

	var err error

	c.storeDir, err = os.MkdirTemp("", "graphite-clickhouse")
	if err != nil {
		return err
	}

	c.address, err = getFreeTCPPort("")
	if err != nil {
		c.Cleanup()
		return err
	}

	c.TestDir, err = filepath.Abs(testDir)
	if err != nil {
		return err
	}

	name := filepath.Base(c.ConfigTpl)

	tmpl, err := template.New(name).ParseFiles(path.Join(testDir, c.ConfigTpl))
	if err != nil {
		c.Cleanup()
		return err
	}

	param := struct {
		CLICKHOUSE_URL     string
		CLICKHOUSE_TLS_URL string
		PROXY_URL          string
		GCH_ADDR           string
		GCH_DIR            string
		TEST_DIR           string
	}{
		CLICKHOUSE_URL:     chURL,
		CLICKHOUSE_TLS_URL: chTLSURL,
		PROXY_URL:          chProxyURL,
		GCH_ADDR:           c.address,
		GCH_DIR:            c.storeDir,
		TEST_DIR:           c.TestDir,
	}

	c.configFile = path.Join(c.storeDir, "graphite-clickhouse.conf")
	f, err := os.OpenFile(c.configFile, os.O_WRONLY|os.O_CREATE, 0644)

	if err != nil {
		c.Cleanup()
		return err
	}

	err = tmpl.ExecuteTemplate(f, name, param)
	if err != nil {
		c.Cleanup()
		return err
	}

	c.cmd = exec.Command(c.Binary, "-config", c.configFile)
	c.cmd.Stdout = os.Stdout
	c.cmd.Stderr = os.Stderr

	if c.TZ != "" {
		c.cmd.Env = append(c.cmd.Env, "TZ="+c.TZ)
	}

	err = c.cmd.Start()
	if err != nil {
		c.Cleanup()
		return err
	}

	return nil
}

func (c *GraphiteClickhouse) Alive() bool {
	if c.cmd == nil {
		return false
	}

	_, _, _, err := client.MetricsFind(http.DefaultClient, "http://"+c.address+"/alive", client.FormatDefault, "NonExistentTarget", 0, 0)

	return err == nil
}

func (c *GraphiteClickhouse) Stop(cleanup bool) error {
	if cleanup {
		defer c.Cleanup()
	}

	if c.cmd == nil {
		return nil
	}

	var err error
	if err = c.cmd.Process.Kill(); err == nil {
		if err = c.cmd.Wait(); err != nil {
			if exitErr, ok := err.(*exec.ExitError); ok {
				if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
					ec := status.ExitStatus()
					if ec == 0 || ec == -1 {
						return nil
					}
				}
			}
		}
	}

	return err
}

func (c *GraphiteClickhouse) Cleanup() {
	if len(c.storeDir) > 0 {
		os.RemoveAll(c.storeDir)
		c.storeDir = ""
		c.cmd = nil
	}
}

func (c *GraphiteClickhouse) URL() string {
	return "http://" + c.address
}

func (c *GraphiteClickhouse) Cmd() string {
	return strings.Join(c.cmd.Args, " ")
}

func (c *GraphiteClickhouse) Grep(s string) {
	out, _ := exec.Command("grep", "-F", s, c.storeDir+"/graphite-clickhouse.log").Output()
	fmt.Fprintf(os.Stderr, "GREP %s", stringutils.UnsafeString(out))
}


================================================
FILE: cmd/e2e-test/main.go
================================================
package main

import (
	"flag"
	"log"
	"os"
	"path"
	"runtime"
	"time"

	"go.uber.org/zap"
)

type MainConfig struct {
	Test *TestSchema `toml:"test"`
}

func IsDir(filename string) (bool, error) {
	info, err := os.Stat(filename)
	if os.IsNotExist(err) {
		return false, nil
	} else if err != nil {
		return false, err
	}

	return info.IsDir(), nil
}

func expandDir(dirname string, paths *[]string) error {
	files, err := os.ReadDir(dirname)
	if err != nil {
		return err
	}

	for _, file := range files {
		if file.IsDir() {
			if err = expandDir(path.Join(dirname, file.Name()), paths); err != nil {
				return err
			}
		} else {
			ext := path.Ext(file.Name())
			if ext == ".toml" {
				*paths = append(*paths, path.Join(dirname, file.Name()))
			}
		}
	}

	return nil
}

func expandFilename(filename string, paths *[]string) error {
	if len(filename) == 0 {
		return nil
	}

	isDir, err := IsDir(filename)
	if err == nil {
		if isDir {
			if err = expandDir(filename, paths); err != nil {
				return err
			}
		} else {
			*paths = append(*paths, filename)
		}
	}

	return err
}

func main() {
	_, filename, _, _ := runtime.Caller(0)
	rootDir := path.Dir(path.Dir(path.Dir(filename))) // carbon-clickhouse repositiry root dir

	config := flag.String("config", "", "toml configuration file or dir where toml files is searched (recursieve)")
	verbose := flag.Bool("verbose", false, "verbose")
	breakOnError := flag.Bool("break", false, "break and wait user response if request failed")
	abortOnError := flag.Bool("abort", false, "abort tests if test failed")
	cleanup := flag.Bool("cleanup", false, "delete containers if exists before start")
	rmi := flag.Bool("rmi", false, "delete images after test end (for low space usage))")
	flag.Parse()

	logger, err := zap.NewProduction()
	if err != nil {
		log.Fatal(err)
	}

	DockerBinary = os.Getenv("DOCKER_E2E")
	if DockerBinary == "" {
		DockerBinary = "docker"
	}

	if *cleanup {
		if exist, _ := containerExist(CchContainerName); exist {
			if ok, out := containerRemove(CchContainerName); !ok {
				logger.Fatal("failed to cleanup",
					zap.String("container", CchContainerName),
					zap.String("error", out),
				)
			}
		}

		if exist, _ := containerExist(ClickhouseContainerName); exist {
			if ok, out := containerRemove(ClickhouseContainerName); !ok {
				logger.Fatal("failed to cleanup",
					zap.String("container", ClickhouseContainerName),
					zap.String("error", out),
				)
			}
		}

		if *config == "" {
			return
		}
	}

	var allConfigs []string

	err = expandFilename(*config, &allConfigs)
	if err != nil {
		logger.Fatal(
			"config",
			zap.Error(err),
		)
	}

	if len(allConfigs) == 0 {
		logger.Fatal("config should be non-null")
	}

	chVersions := make(map[string]Clickhouse)
	configs := make([]*MainConfig, 0, len(allConfigs))

	for _, config := range allConfigs {
		cfg, err := loadConfig(config, rootDir)
		if err == nil {
			configs = append(configs, cfg)

			for _, ch := range cfg.Test.Clickhouse {
				chVersions[ch.Key()] = ch
			}

			now := time.Now()
			if !initTest(cfg, rootDir, now, *verbose, *breakOnError, logger) {
				os.Exit(1)
			}
		} else {
			logger.Error("failed to read config",
				zap.String("config", config),
				zap.Error(err),
				zap.Any("decode", cfg),
			)
		}
	}

	failed := 0
	total := 0
	verifyCount := 0
	verifyFailed := 0

	_, err = cmdExec(DockerBinary, "network", "inspect", DockerNetwork)
	if err != nil {
		out, err := cmdExec(DockerBinary, "network", "create", DockerNetwork)
		if err != nil {
			logger.Error("failed to create network",
				zap.Error(err),
				zap.String("out", out),
			)
			os.Exit(1)
		}
	}

	for chVersion := range chVersions {
		ch := chVersions[chVersion]

		if exist, out := containerExist(ClickhouseContainerName); exist {
			logger.Error("clickhouse already exist",
				zap.String("container", ClickhouseContainerName),
				zap.String("out", out),
			)
			os.Exit(1)
		}

		logger.Info("clickhouse",
			zap.Any("clickhouse image", ch.DockerImage),
			zap.Any("clickhouse version", ch.Version),
			zap.String("clickhouse config", ch.Dir),
			zap.String("tz", ch.TZ),
		)

		if clickhouseStart(&ch, logger) {
			time.Sleep(100 * time.Millisecond)

			for i := 200; i < 3000; i += 200 {
				if ch.Alive() {
					break
				}

				time.Sleep(time.Duration(i) * time.Millisecond)
			}

			if !ch.Alive() {
				logger.Error("starting clickhouse",
					zap.Any("clickhouse version", ch.Version),
					zap.String("clickhouse config", ch.Dir),
					zap.String("error", "clickhouse is down"),
				)

				failed++
				total++
				verifyCount++
				verifyFailed++
			} else {
				for _, config := range configs {
					if config.Test.chVersions[chVersion] {
						now := time.Now()
						if initTest(config, rootDir, now, *verbose, *breakOnError, logger) {
							testFailed, testTotal, vCount, vFailed := runTest(config, &ch, rootDir, now, *verbose, *breakOnError, logger)
							failed += testFailed
							total += testTotal
							verifyCount += vCount
							verifyFailed += vFailed
						} else {
							failed++
							total++
							verifyCount++
							verifyFailed++
						}
					}
				}
			}

			if !clickhouseStop(&ch, logger) {
				failed++
				verifyFailed++
			}
		} else {
			failed++
			total++
			verifyCount++
			verifyFailed++
		}

		if *rmi {
			if success, out := imageDelete(ch.DockerImage, ch.Version); !success {
				logger.Error("docker remove image",
					zap.Any("clickhouse version", ch.Version),
					zap.String("clickhouse config", ch.Dir),
					zap.String("out", out),
				)
			}
		}

		if *abortOnError && failed > 0 {
			break
		}
	}

	if failed > 0 {
		logger.Error("tests ended",
			zap.String("status", "failed"),
			zap.Int("test_count", total),
			zap.Int("test_failed", failed),
			zap.Int("checks", verifyCount),
			zap.Int("failed", verifyFailed),
			zap.Int("configs", len(allConfigs)),
		)
		os.Exit(1)
	} else {
		logger.Info("tests ended",
			zap.String("status", "success"),
			zap.Int("test_count", total),
			zap.Int("test_failed", failed),
			zap.Int("checks", verifyCount),
			zap.Int("failed", verifyFailed),
			zap.Int("configs", len(allConfigs)),
		)
	}
}


================================================
FILE: cmd/e2e-test/rproxy.go
================================================
package main

import (
	"errors"
	"net/http"
	"net/http/httptest"
	"net/http/httputil"
	"net/url"
	"sync"
	"sync/atomic"
	"time"

	"github.com/lomik/graphite-clickhouse/pkg/dry"
)

type AtomicDuration struct {
	val int64
}

func (d *AtomicDuration) Store(duration time.Duration) {
	atomic.StoreInt64(&d.val, duration.Nanoseconds())
}

func (d *AtomicDuration) Load() time.Duration {
	return time.Duration(atomic.LoadInt64(&d.val))
}

func (d *AtomicDuration) MarshalText() ([]byte, error) {
	s := d.Load().String()
	return dry.UnsafeStringBytes(&s), nil
}

func (d *AtomicDuration) UnmarshalText(b []byte) error {
	val, err := time.ParseDuration(dry.UnsafeString(b))
	if err != nil {
		return err
	}

	d.Store(val)

	return nil
}

type HttpReverseProxy struct {
	Delay               AtomicDuration `toml:"delay"`
	BreakWithStatusCode int64          `toml:"break_with_status_code"`

	srv    *httptest.Server
	remote *url.URL
	wg     sync.WaitGroup
}

func (p *HttpReverseProxy) Start(remoteURL string) (err error) {
	if p.srv != nil {
		err = errors.New("reverse proxy already started")
		return
	}

	if p.BreakWithStatusCode < 0 {
		p.BreakWithStatusCode = 0
	}

	if p.remote, err = url.Parse(remoteURL); err != nil {
		err = errors.New("reverse proxy already started")
		return
	}

	p.srv = httptest.NewUnstartedServer(p)

	p.wg.Add(1)

	go func() {
		defer p.wg.Done()

		p.srv.Start()
	}()

	return
}

func (p *HttpReverseProxy) Stop() {
	if p.srv == nil {
		return
	}

	p.srv.CloseClientConnections()
	p.srv.Close()
	p.wg.Wait()
	p.srv = nil
}

func (p *HttpReverseProxy) URL() string {
	return p.srv.URL
}

func (p *HttpReverseProxy) SetDelay(delay time.Duration) {
	p.Delay.Store(delay)
}

func (p *HttpReverseProxy) GetDelay() time.Duration {
	return p.Delay.Load()
}

func (p *HttpReverseProxy) SetBreakStatusCode(statusCode int) {
	atomic.StoreInt64(&p.BreakWithStatusCode, int64(statusCode))
}

func (p *HttpReverseProxy) GetBreakStatusCode() int {
	return int(atomic.LoadInt64(&p.BreakWithStatusCode))
}

func (p *HttpReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	r.Host = p.remote.Host

	delay := p.GetDelay()
	if delay != 0 {
		time.Sleep(delay)
	}

	breakWithStatusCode := p.GetBreakStatusCode()
	if breakWithStatusCode != 0 {
		http.Error(w, "", breakWithStatusCode)
		return
	}

	proxy := httputil.NewSingleHostReverseProxy(p.remote)
	proxy.ServeHTTP(w, r)
}


================================================
FILE: cmd/e2e-test/utils.go
================================================
package main

import "os/exec"

func cmdExec(programm string, args ...string) (string, error) {
	cmd := exec.Command(programm, args...)
	out, err := cmd.CombinedOutput()

	return string(out), err
}


================================================
FILE: cmd/graphite-clickhouse-client/main.go
================================================
package main

import (
	"flag"
	"fmt"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/go-graphite/protocol/carbonapi_v3_pb"
	"github.com/lomik/graphite-clickhouse/helper/client"
	"github.com/lomik/graphite-clickhouse/helper/datetime"
)

type StringSlice []string

func (u *StringSlice) Set(value string) error {
	*u = append(*u, value)
	return nil
}

func (u *StringSlice) String() string {
	return "[ " + strings.Join(*u, ", ") + " ]"
}

func (u *StringSlice) Type() string {
	return "[]string"
}

func main() {
	address := flag.String("address", "http://127.0.0.1:9090", "Address of graphite-clickhouse server")
	fromStr := flag.String("from", "0", "from")
	untilStr := flag.String("until", "", "until")
	maxDataPointsStr := flag.String("maxDataPoints", "1048576", "Maximum amount of datapoints in response")

	metricsFind := flag.String("find", "", "Query for /metrics/find/ , valid formats are carbonapi_v3_pb. protobuf, pickle")

	tagsValues := flag.String("tags_values", "", "Query for /tags/autoComplete/values (with query like 'searchTag[=valuePrefix];tag1=value1;tag2=~value*' or '<>' for empty)")
	tagsNames := flag.String("tags_names", "", "Query for /tags/autoComplete/tags (with query like '[tagPrefix];tag1=value1;tag2=~value*[' or '<>' for empty)")
	limit := flag.Uint64("limit", 0, "limit for some queries (tags_values, tags_values)")

	timeout := flag.Duration("timeout", time.Minute, "request timeout")

	var targets StringSlice

	flag.Var(&targets, "target", "Target for /render")

	format := client.FormatDefault
	flag.Var(&format, "format", fmt.Sprintf("Response format %v", client.FormatTypes()))

	flag.Parse()

	ec := 0

	tz, err := datetime.Timezone("")
	if err != nil {
		fmt.Printf("can't get timezone: %s\n", err.Error())
		os.Exit(1)
	}

	now := time.Now()

	from := datetime.DateParamToEpoch(*fromStr, tz, now, 0)
	if from == 0 && len(targets) > 0 {
		fmt.Printf("invalid from: %s\n", *fromStr)
		os.Exit(1)
	}

	var until int64

	if *untilStr == "" && len(targets) > 0 {
		*untilStr = "now"
	}

	until = datetime.DateParamToEpoch(*untilStr, tz, now, 0)
	if until == 0 && len(targets) > 0 {
		fmt.Printf("invalid until: %s\n", *untilStr)
		os.Exit(1)
	}

	maxDataPoints, err := strconv.ParseInt(*maxDataPointsStr, 10, 64)
	if err != nil {
		fmt.Printf("invalid maxDataPoints: %s\n", *maxDataPointsStr)
		os.Exit(1)
	}

	httpClient := http.Client{
		Timeout: *timeout,
	}

	if *metricsFind != "" {
		formatFind := format
		if formatFind == client.FormatDefault {
			formatFind = client.FormatPb_v3
		}

		queryRaw, r, respHeader, err := client.MetricsFind(&httpClient, *address, formatFind, *metricsFind, from, until)
		if respHeader != nil {
			fmt.Printf("Responce header: %+v\n", respHeader)
		}

		fmt.Print("'")
		fmt.Print(queryRaw)
		fmt.Print("' = ")

		if err == nil {
			if len(r) > 0 {
				fmt.Println("[")

				for i, m := range r {
					fmt.Printf("  { Path: '%s', IsLeaf: %v }", m.Path, m.IsLeaf)

					if i < len(r)-1 {
						fmt.Println(",")
					} else {
						fmt.Println("")
					}
				}

				fmt.Println("]")
			} else {
				fmt.Println("[]")
			}
		} else {
			ec = 1

			fmt.Printf("'%s'\n", strings.TrimRight(err.Error(), "\n"))
		}
	}

	if *tagsValues != "" {
		formatTags := format
		if formatTags == client.FormatDefault {
			formatTags = client.FormatJSON
		}

		queryRaw, r, respHeader, err := client.TagsValues(&httpClient, *address, formatTags, *tagsValues, *limit, from, until)
		if respHeader != nil {
			fmt.Printf("Responce header: %+v\n", respHeader)
		}

		fmt.Print("'")
		fmt.Print(queryRaw)
		fmt.Print("' = ")

		if err == nil {
			if len(r) > 0 {
				fmt.Println("[")

				for i, v := range r {
					fmt.Printf("  { Value: '%s' }", v)

					if i < len(r)-1 {
						fmt.Println(",")
					} else {
						fmt.Println("")
					}
				}

				fmt.Println("]")
			} else {
				fmt.Println("[]")
			}
		} else {
			ec = 1

			fmt.Printf("'%s'\n", strings.TrimRight(err.Error(), "\n"))
		}
	}

	if *tagsNames != "" {
		formatTags := format
		if formatTags == client.FormatDefault {
			formatTags = client.FormatJSON
		}

		queryRaw, r, respHeader, err := client.TagsNames(&httpClient, *address, formatTags, *tagsNames, *limit, from, until)
		if respHeader != nil {
			fmt.Printf("Responce header: %+v\n", respHeader)
		}

		fmt.Print("'")
		fmt.Print(queryRaw)
		fmt.Print("' = ")

		if err == nil {
			if len(r) > 0 {
				fmt.Println("[")

				for i, v := range r {
					fmt.Printf("  { Tag: '%s' }", v)

					if i < len(r)-1 {
						fmt.Println(",")
					} else {
						fmt.Println("")
					}
				}

				fmt.Println("]")
			} else {
				fmt.Println("[]")
			}
		} else {
			ec = 1

			fmt.Printf("'%s'\n", strings.TrimRight(err.Error(), "\n"))
		}
	}

	if len(targets) > 0 {
		formatRender := format
		if formatRender == client.FormatDefault {
			formatRender = client.FormatPb_v3
		}

		queryRaw, r, respHeader, err := client.Render(&httpClient, *address, formatRender, targets, []*carbonapi_v3_pb.FilteringFunction{}, maxDataPoints, from, until)
		if respHeader != nil {
			fmt.Printf("Responce header: %+v\n", respHeader)
		}

		fmt.Print("'")
		fmt.Print(queryRaw)
		fmt.Print("' = ")

		if err == nil {
			if len(r) > 0 {
				fmt.Println("[")

				for i, m := range r {
					fmt.Println("  {")
					fmt.Printf("    Name: '%s', PathExpression: '%v',\n", m.Name, m.PathExpression)
					fmt.Printf("    ConsolidationFunc: %s, XFilesFactor: %f, AppliedFunctions: %s,\n", m.ConsolidationFunc, m.XFilesFactor, m.AppliedFunctions)
					fmt.Printf("    Start: %d, Stop: %d, Step: %d, RequestStart: %d, RequestStop: %d,\n", m.StartTime, m.StopTime, m.StepTime, m.RequestStartTime, m.RequestStopTime)
					fmt.Printf("    Values: %+v\n", m.Values)

					if i == len(r) {
						fmt.Println("  }")
					} else {
						fmt.Println("  },")
					}
				}

				fmt.Println("]")
			} else {
				fmt.Println("[]")
			}
		} else {
			ec = 1

			fmt.Printf("'%s'\n", strings.TrimRight(err.Error(), "\n"))
		}
	}

	os.Exit(ec)
}


================================================
FILE: config/.gitignore
================================================
tests_tmp/


================================================
FILE: config/config.go
================================================
package config

import (
	"bytes"
	"crypto/tls"
	"fmt"
	"net"
	"net/url"
	"os"
	"reflect"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/cactus/go-statsd-client/v5/statsd"
	"github.com/lomik/carbon-clickhouse/helper/config"
	"github.com/msaf1980/go-metrics/graphite"
	"github.com/msaf1980/go-timeutils/duration"
	toml "github.com/pelletier/go-toml"
	"github.com/pkg/errors"
	"go.uber.org/zap"

	"github.com/lomik/zapwriter"

	"github.com/lomik/graphite-clickhouse/cache"
	"github.com/lomik/graphite-clickhouse/helper/clickhouse"
	"github.com/lomik/graphite-clickhouse/helper/date"
	"github.com/lomik/graphite-clickhouse/helper/rollup"
	"github.com/lomik/graphite-clickhouse/limiter"
	"github.com/lomik/graphite-clickhouse/metrics"
)

type SDType uint8

const (
	SDNone  SDType = iota
	SDNginx        // https://github.com/weibocom/nginx-upsync-module
)

var sdTypeStrings map[SDType]string = map[SDType]string{SDNone: "", SDNginx: "nginx"}

func (a *SDType) Set(value string) error {
	switch value {
	case "nginx":
		*a = SDNginx
	case "", "0":
		*a = SDNone
	default:
		return fmt.Errorf("invalid sd type %q", value)
	}

	return nil
}

func (a *SDType) UnmarshalText(data []byte) error {
	return a.Set(string(data))
}

func (a *SDType) MarshalText() ([]byte, error) {
	return []byte(a.String()), nil
}

func (a *SDType) UnmarshalJSON(data []byte) error {
	return a.Set(string(data))
}

func (a *SDType) MarshalJSON() ([]byte, error) {
	return []byte(a.String()), nil
}

func (a *SDType) String() string {
	return sdTypeStrings[*a]
}

func (a *SDType) Type() string {
	return "service_discovery_type"
}

// Cache config
type CacheConfig struct {
	Type                string        `toml:"type"              json:"type"              comment:"cache type"`
	Size                int           `toml:"size-mb"           json:"size-mb"           comment:"cache size"`
	MemcachedServers    []string      `toml:"memcached-servers" json:"memcached-servers" comment:"memcached servers"`
	DefaultTimeoutSec   int32         `toml:"default-timeout"   json:"default-timeout"   comment:"default cache ttl"`
	DefaultTimeoutStr   string        `toml:"-"                 json:"-"`
	ShortTimeoutSec     int32         `toml:"short-timeout"     json:"short-timeout"     comment:"short-time cache ttl"`
	ShortTimeoutStr     string        `toml:"-"                 json:"-"`
	FindTimeoutSec      int32         `toml:"find-timeout"      json:"find-timeout"      comment:"finder/tags autocompleter cache ttl"`
	ShortDuration       time.Duration `toml:"short-duration"    json:"short-duration"    comment:"maximum diration, used with short_timeout"`
	ShortUntilOffsetSec int64         `toml:"short-offset"      json:"short-offset"      comment:"offset beetween now and until for select short cache timeout"`
}

// Common config
type Common struct {
	Listen                 string           `toml:"listen"                     json:"listen"                     comment:"general listener"`
	PprofListen            string           `toml:"pprof-listen"               json:"pprof-listen"               comment:"listener to serve /debug/pprof requests. '-pprof' argument overrides it"`
	MaxCPU                 int              `toml:"max-cpu"                    json:"max-cpu"`
	MaxMetricsInFindAnswer int              `toml:"max-metrics-in-find-answer" json:"max-metrics-in-find-answer" comment:"limit number of results from find query, 0=unlimited"`
	MaxMetricsPerTarget    int              `toml:"max-metrics-per-target"     json:"max-metrics-per-target"     comment:"limit numbers of queried metrics per target in /render requests, 0 or negative = unlimited"`
	AppendEmptySeries      bool             `toml:"append-empty-series"        json:"append-empty-series"        comment:"if true, always return points for all metrics, replacing empty results with list of NaN"`
	TargetBlacklist        []string         `toml:"target-blacklist"           json:"target-blacklist"           comment:"daemon returns empty response if query matches any of regular expressions"                  commented:"true"`
	Blacklist              []*regexp.Regexp `toml:"-"                          json:"-"` // compiled TargetBlacklist
	MemoryReturnInterval   time.Duration    `toml:"memory-return-interval"     json:"memory-return-interval"     comment:"daemon will return the freed memory to the OS when it>0"`
	HeadersToLog           []string         `toml:"headers-to-log"             json:"headers-to-log"             comment:"additional request headers to log"`

	BaseWeight       int           `toml:"base_weight"            json:"base_weight"            comment:"service discovery base weight (on idle)"`
	DegragedMultiply float64       `toml:"degraged-multiply"            json:"degraged-multiply"            comment:"service discovery degraded load avg multiplier (if normalized load avg > degraged_load_avg) (default 4.0)"`
	DegragedLoad     float64       `toml:"degraged-load-avg"            json:"degraged-load-avg"            comment:"service discovery normilized load avg degraded point (default 1.0)"`
	SDType           SDType        `toml:"service-discovery-type" json:"service-discovery-type" comment:"service discovery type"`
	SD               string        `toml:"service-discovery"      json:"service-discovery"      comment:"service discovery address (consul)"`
	SDNamespace      string        `toml:"service-discovery-ns"   json:"service-discovery-ns"   comment:"service discovery namespace (graphite by default)"`
	SDDc             []string      `toml:"service-discovery-ds"   json:"service-discovery-ds"   comment:"service discovery datacenters (first - is primary, in other register as backup)"`
	SDExpire         time.Duration `toml:"service-discovery-expire"   json:"service-discovery-expire"   comment:"service discovery expire duration for cleanup (minimum is 24h, if enabled)"`

	FindCacheConfig CacheConfig `toml:"find-cache"      json:"find-cache"             comment:"find/tags cache config"`

	FindCache cache.BytesCache `toml:"-" json:"-"`
}

// FeatureFlags contains feature flags that significantly change how gch responds to some requests
type FeatureFlags struct {
	UseCarbonBehavior    bool `toml:"use-carbon-behaviour" json:"use-carbon-behaviour" comment:"if true, prefers carbon's behaviour on how tags are treated"`
	DontMatchMissingTags bool `toml:"dont-match-missing-tags" json:"dont-match-missing-tags" comment:"if true, seriesByTag terms containing '!=' or '!=~' operators will not match metrics that don't have the tag at all"`
	LogQueryProgress     bool `toml:"log-query-progress" json:"log-query-progress" comment:"if true, gch will log affected rows count by clickhouse query"`
}

// IndexReverseRule contains rules to use direct or reversed request to index table
type IndexReverseRule struct {
	Suffix   string         `toml:"suffix,omitempty" json:"suffix"  comment:"rule is used when the target suffix is matched"`
	Prefix   string         `toml:"prefix,omitempty" json:"prefix"  comment:"rule is used when the target prefix is matched"`
	RegexStr string         `toml:"regex,omitempty"  json:"regex"   comment:"rule is used when the target regex is matched"`
	Regex    *regexp.Regexp `toml:"-"                json:"-"`
	Reverse  string         `toml:"reverse"          json:"reverse" comment:"same as index-reverse"`
}

type Costs struct {
	Cost       *int           `toml:"cost"        json:"cost"        comment:"default cost (for wildcarded equalence or matched with regex, or if no value cost set)"`
	ValuesCost map[string]int `toml:"values-cost" json:"values-cost" comment:"cost with some value (for equalence without wildcards) (additional tuning, usually not needed)"`
}

// IndexReverses is a slise of ptrs to IndexReverseRule
type IndexReverses []*IndexReverseRule

const (
	IndexAuto     = iota
	IndexDirect   = iota
	IndexReversed = iota
)

// IndexReverse maps setting name to value
var IndexReverse = map[string]uint8{
	"direct":   IndexDirect,
	"auto":     IndexAuto,
	"reversed": IndexReversed,
}

// IndexReverseNames contains valid names for index-reverse setting
var IndexReverseNames = []string{"auto", "direct", "reversed"}

type UserLimits struct {
	MaxQueries        int `toml:"max-queries"      json:"max-queries"  comment:"Max queries to fetch data"`
	ConcurrentQueries int `toml:"concurrent-queries" json:"concurrent-queries" comment:"Concurrent queries to fetch data"`
	AdaptiveQueries   int `toml:"adaptive-queries" json:"adaptive-queries" comment:"Adaptive queries (based on load average) for increase/decrease concurrent queries"`

	Limiter limiter.ServerLimiter `toml:"-" json:"-"`
}

type QueryParam struct {
	Duration    time.Duration `toml:"duration"     json:"duration"     comment:"minimal duration (beetween from/until) for select query params"`
	URL         string        `toml:"url"          json:"url"          comment:"url for queries with durations greater or equal than"`
	DataTimeout time.Duration `toml:"data-timeout" json:"data-timeout" comment:"total timeout to fetch data"`

	MaxQueries        int `toml:"max-queries" json:"max-queries" comment:"Max queries to fetch data"`
	ConcurrentQueries int `toml:"concurrent-queries" json:"concurrent-queries" comment:"Concurrent queries to fetch data"`
	AdaptiveQueries   int `toml:"adaptive-queries" json:"adaptive-queries" comment:"Adaptive queries (based on load average) for increase/decrease concurrent queries"`

	Limiter limiter.ServerLimiter `toml:"-" json:"-"`
}

func binarySearchQueryParamLe(a []QueryParam, duration time.Duration, start, end int) int {
	length := end - start
	if length <= 0 {
		return -1 // not found
	} else if length == 1 {
		if a[start].Duration > duration {
			return -1
		}

		return start
	}

	var result int

	mid := start + length/2
	if a[mid].Duration > duration {
		result = binarySearchQueryParamLe(a, duration, start, mid)
	} else {
		if result = binarySearchQueryParamLe(a, duration, mid+1, end); result == -1 {
			result = mid
		}
	}

	return result
}

// ClickHouse config
type ClickHouse struct {
	URL         string        `toml:"url"                      json:"url"                      comment:"default url, see https://clickhouse.tech/docs/en/interfaces/http. Can be overwritten with query-params"`
	DataTimeout time.Duration `toml:"data-timeout"             json:"data-timeout"             comment:"default total timeout to fetch data, can be overwritten with query-params"`
	QueryParams []QueryParam  `toml:"query-params"             json:"query-params"             comment:"customized query params (url, data timeout, limiters) for durations greater or equal"`

	ProgressSendingInterval time.Duration `toml:"progress-sending-interval" json:"progress-sending-interval" comment:"time interval for ch query progress sending, it's equal to http_headers_progress_interval_ms header"`

	RenderMaxQueries        int `toml:"render-max-queries" json:"render-max-queries" comment:"Max queries to render queiries"`
	RenderConcurrentQueries int `toml:"render-concurrent-queries" json:"render-concurrent-queries" comment:"Concurrent queries to render queiries"`
	RenderAdaptiveQueries   int `toml:"render-adaptive-queries" json:"render-adaptive-queries" comment:"Render adaptive queries (based on load average) for increase/decrease concurrent queries"`

	FindMaxQueries        int                   `toml:"find-max-queries" json:"find-max-queries" comment:"Max queries for find queries"`
	FindConcurrentQueries int                   `toml:"find-concurrent-queries" json:"find-concurrent-queries" comment:"Find concurrent queries for find queries"`
	FindAdaptiveQueries   int                   `toml:"find-adaptive-queries" json:"find-adaptive-queries" comment:"Find adaptive queries (based on load average) for increase/decrease concurrent queries"`
	FindLimiter           limiter.ServerLimiter `toml:"-"                        json:"-"`

	TagsMaxQueries        int                   `toml:"tags-max-queries" json:"tags-max-queries" comment:"Max queries for tags queries"`
	TagsConcurrentQueries int                   `toml:"tags-concurrent-queries" json:"tags-concurrent-queries" comment:"Concurrent queries for tags queries"`
	TagsAdaptiveQueries   int                   `toml:"tags-adaptive-queries" json:"tags-adaptive-queries" comment:"Tags adaptive queries (based on load average) for increase/decrease concurrent queries"`
	TagsLimiter           limiter.ServerLimiter `toml:"-"                        json:"-"`

	WildcardMinDistance   int  `toml:"wildcard-min-distance" json:"wildcard-min-distance" comment:"If a wildcard appears both at the start and the end of a plain query at a distance (in terms of nodes) less than wildcard-min-distance, then it will be discarded. This parameter can be used to discard expensive queries."`
	TrySplitQuery         bool `toml:"try-split-query" json:"try-split-query" comment:"Plain queries like '{first,second}.custom.metric.*' are also a subject to wildcard-min-distance restriction. But can be split into 2 queries: 'first.custom.metric.*', 'second.custom.metric.*'. Note that: only one list will be split; if there are wildcard in query before (after) list then reverse (direct) notation will be preferred; if there are wildcards before and after list, then query will not be split"`
	MaxNodeToSplitIndex   int  `toml:"max-node-to-split-index" json:"max-node-to-split-index" comment:"Used only if try-split-query is true. Query that contains list will be split if its (list) node index is less or equal to max-node-to-split-index. By default is 0. It is recommended to have this value set to 2 or 3 and increase it very carefully, because 3 or 4 plain nodes without wildcards have good selectivity"`
	TagsMinInQuery        int  `toml:"tags-min-in-query" json:"tags-min-in-query" comment:"Minimum tags in seriesByTag query"`
	TagsMinInAutocomplete int  `toml:"tags-min-in-autocomplete" json:"tags-min-in-autocomplete" comment:"Minimum tags in autocomplete query"`

	UserLimits           map[string]UserLimits `toml:"user-limits"              json:"user-limits"              comment:"customized query limiter for some users"                                                                                        commented:"true"`
	DateFormat           string                `toml:"date-format"              json:"date-format"              comment:"Date format (default, utc, both)"`
	IndexTable           string                `toml:"index-table"              json:"index-table"              comment:"see doc/index-table.md"`
	IndexUseDaily        bool                  `toml:"index-use-daily"          json:"index-use-daily"`
	IndexReverse         string                `toml:"index-reverse"            json:"index-reverse"            comment:"see doc/config.md"`
	IndexReverses        IndexReverses         `toml:"index-reverses"           json:"index-reverses"           comment:"see doc/config.md"                                                                                                              commented:"true"`
	IndexTimeout         time.Duration         `toml:"index-timeout"            json:"index-timeout"            comment:"total timeout to fetch series list from index"`
	TaggedTable          string                `toml:"tagged-table"             json:"tagged-table"             comment:"'tagged' table from carbon-clickhouse, required for seriesByTag"`
	TagsCountTable       string                `toml:"tags-count-table"         json:"tags-count-table"         comment:"Table that contains the total amounts of each tag-value pair. It is used to avoid usage of high cardinality tag-value pairs when querying TaggedTable. If left empty, basic sorting will be used. See more detailed description in doc/config.md"`
	TaggedAutocompleDays int                   `toml:"tagged-autocomplete-days" json:"tagged-autocomplete-days" comment:"or how long the daemon will query tags during autocomplete"`
	TaggedUseDaily       bool                  `toml:"tagged-use-daily"         json:"tagged-use-daily"         comment:"whether to use date filter when searching for the metrics in the tagged-table"`
	TaggedCosts          map[string]*Costs     `toml:"tagged-costs"             json:"tagged-costs"             comment:"costs for tags (for tune which tag will be used as primary), by default is 0, increase for costly (with poor selectivity) tags" commented:"true"`
	TreeTable            string                `toml:"tree-table"               json:"tree-table"               comment:"old index table, DEPRECATED, see description in doc/config.md"                                                                  commented:"true"`
	ReverseTreeTable     string                `toml:"reverse-tree-table"       json:"reverse-tree-table"                                                                                                                                                commented:"true"`
	DateTreeTable        string                `toml:"date-tree-table"          json:"date-tree-table"                                                                                                                                                   commented:"true"`
	DateTreeTableVersion int                   `toml:"date-tree-table-version"  json:"date-tree-table-version"                                                                                                                                           commented:"true"`
	TreeTimeout          time.Duration         `toml:"tree-timeout"             json:"tree-timeout"                                                                                                                                                      commented:"true"`
	TagTable             string                `toml:"tag-table"                json:"tag-table"                comment:"is not recommended to use, https://github.com/lomik/graphite-clickhouse/wiki/TagsRU"                                            commented:"true"`
	ExtraPrefix          string                `toml:"extra-prefix"             json:"extra-prefix"             comment:"add extra prefix (directory in graphite) for all metrics, w/o trailing dot"`
	ConnectTimeout       time.Duration         `toml:"connect-timeout"          json:"connect-timeout"          comment:"TCP connection timeout"`
	// TODO: remove in v0.14
	DataTableLegacy string `toml:"data-table"               json:"data-table"               comment:"will be removed in 0.14"                                                                                                        commented:"true"`
	// TODO: remove in v0.14
	RollupConfLegacy string `toml:"rollup-conf"              json:"-"                                                                                                                                                                 commented:"true"`
	MaxDataPoints    int    `toml:"max-data-points"          json:"max-data-points"          comment:"max points per metric when internal-aggregation=true"`
	// InternalAggregation controls if ClickHouse itself or graphite-clickhouse aggregates points to proper retention
	InternalAggregation bool `toml:"internal-aggregation"     json:"internal-aggregation"     comment:"ClickHouse-side aggregation, see doc/aggregation.md"`

	TLSParams config.TLS  `toml:"tls"                      json:"tls"                      comment:"mTLS HTTPS configuration for connecting to clickhouse server"                                                                         commented:"true"`
	TLSConfig *tls.Config `toml:"-"                        json:"-"`
}

func clickhouseURLValidate(chURL string) (*url.URL, error) {
	u, err := url.Parse(chURL)
	if err != nil {
		return nil, fmt.Errorf("error %q in url %q", err.Error(), chURL)
	} else if u.Scheme != "http" && u.Scheme != "https" {
		return nil, fmt.Errorf("scheme not supported in url %q", chURL)
	} else if strings.Contains(u.RawQuery, " ") {
		return nil, fmt.Errorf("space not allowed in url %q", chURL)
	}

	return u, nil
}

// Tags config
type Tags struct {
	Rules             string                     `toml:"rules"               json:"rules"`
	Date              string                     `toml:"date"                json:"date"`
	ExtraWhere        string                     `toml:"extra-where"         json:"extra-where"`
	InputFile         string                     `toml:"input-file"          json:"input-file"`
	OutputFile        string                     `toml:"output-file"         json:"output-file"`
	Threads           int                        `toml:"threads"             json:"threads"              comment:"number of threads for uploading tags to clickhouse (1 by default)"`
	Compression       clickhouse.ContentEncoding `toml:"compression"         json:"compression"          comment:"compression method for tags before sending them to clickhouse (i.e. content encoding): gzip (default), none, zstd"`
	Version           uint32                     `toml:"version"             json:"version"              comment:"fixed tags version for testing purposes (by default the current timestamp is used for each upload)"`
	SelectChunksCount int                        `toml:"select-chunks-count" json:"select-chunks-count"  comment:"number of chunks for selecting metrics from clickhouse (10 by default)"`
}

// Carbonlink configuration
type Carbonlink struct {
	Server         string        `toml:"server"              json:"server"`
	Threads        int           `toml:"threads-per-request" json:"threads-per-request"`
	Retries        int           `toml:"-"                   json:"-"`
	ConnectTimeout time.Duration `toml:"connect-timeout"     json:"connect-timeout"`
	QueryTimeout   time.Duration `toml:"query-timeout"       json:"query-timeout"`
	TotalTimeout   time.Duration `toml:"total-timeout"       json:"total-timeout"       comment:"timeout for querying and parsing response"`
}

// Prometheus configuration
type Prometheus struct {
	Listen                     string        `toml:"listen"                        json:"listen"                        comment:"listen addr for prometheus ui and api"`
	ExternalURLRaw             string        `toml:"external-url"                  json:"external-url"                  comment:"allows to set URL for redirect manually"`
	ExternalURL                *url.URL      `toml:"-"                             json:"-"`
	PageTitle                  string        `toml:"page-title"                    json:"page-title"`
	LookbackDelta              time.Duration `toml:"lookback-delta"                json:"lookback-delta"`
	RemoteReadConcurrencyLimit int           `toml:"remote-read-concurrency-limit" json:"remote-read-concurrency-limit" comment:"concurrently handled remote read requests"`
}

const (
	// ContextGraphite for data tables
	ContextGraphite = "graphite"
	// ContextPrometheus for data tables
	ContextPrometheus = "prometheus"
)

var knownDataTableContext = map[string]bool{
	ContextGraphite:   true,
	ContextPrometheus: true,
}

// DataTable configs
type DataTable struct {
	Table                  string                `toml:"table"                    json:"table"                    comment:"data table from carbon-clickhouse"`
	Reverse                bool                  `toml:"reverse"                  json:"reverse"                  comment:"if it stores direct or reversed metrics"`
	MaxAge                 time.Duration         `toml:"max-age"                  json:"max-age"                  comment:"maximum age stored in the table"`
	MinAge                 time.Duration         `toml:"min-age"                  json:"min-age"                  comment:"minimum age stored in the table"`
	MaxInterval            time.Duration         `toml:"max-interval"             json:"max-interval"             comment:"maximum until-from interval allowed for the table"`
	MinInterval            time.Duration         `toml:"min-interval"             json:"min-interval"             comment:"minimum until-from interval allowed for the table"`
	TargetMatchAny         string                `toml:"target-match-any"         json:"target-match-any"         comment:"table allowed only if any metrics in target matches regexp"`
	TargetMatchAll         string                `toml:"target-match-all"         json:"target-match-all"         comment:"table allowed only if all metrics in target matches regexp"`
	TargetMatchAnyRegexp   *regexp.Regexp        `toml:"-"                        json:"-"`
	TargetMatchAllRegexp   *regexp.Regexp        `toml:"-"                        json:"-"`
	RollupConf             string                `toml:"rollup-conf"              json:"-"                        comment:"custom rollup.xml file for table, 'auto' and 'none' are allowed as well"`
	RollupAutoTable        string                `toml:"rollup-auto-table"        json:"rollup-auto-table"        comment:"custom table for 'rollup-conf=auto', useful for Distributed or MatView"`
	RollupAutoInterval     *time.Duration        `toml:"rollup-auto-interval"     json:"rollup-auto-interval"     comment:"rollup update interval for 'rollup-conf=auto'"`
	RollupDefaultPrecision uint32                `toml:"rollup-default-precision" json:"rollup-default-precision" comment:"is used when none of rules match"`
	RollupDefaultFunction  string                `toml:"rollup-default-function"  json:"rollup-default-function"  comment:"is used when none of rules match"`
	RollupUseReverted      bool                  `toml:"rollup-use-reverted"      json:"rollup-use-reverted"      comment:"should be set to true if you don't have reverted regexps in rollup-conf for reversed tables"`
	Context                []string              `toml:"context"                  json:"context"                  comment:"valid values are 'graphite' of 'prometheus'"`
	ContextMap             map[string]bool       `toml:"-"                        json:"-"`
	Rollup                 *rollup.Rollup        `toml:"-"                        json:"rollup-conf"`
	QueryMetrics           *metrics.QueryMetrics `toml:"-"                        json:"-"`
}

// Debug config
type Debug struct {
	Directory     string      `toml:"directory"          json:"directory"          comment:"the directory for additional debug output"`
	DirectoryPerm os.FileMode `toml:"directory-perm"     json:"directory-perm"     comment:"permissions for directory, octal value is set as 0o755"`
	// If ExternalDataPerm > 0 and X-Gch-Debug-Ext-Data HTTP header is set, the external data used in the query
	// will be saved in the DebugDir directory
	ExternalDataPerm os.FileMode `toml:"external-data-perm" json:"external-data-perm" comment:"permissions for directory, octal value is set as 0o640"`
}

// Config is the daemon configuration
type Config struct {
	Common       Common             `toml:"common"        json:"common"`
	FeatureFlags FeatureFlags       `toml:"feature-flags" json:"feature-flags"`
	Metrics      metrics.Config     `toml:"metrics"       json:"metrics"`
	ClickHouse   ClickHouse         `toml:"clickhouse"    json:"clickhouse"`
	DataTable    []DataTable        `toml:"data-table"    json:"data-table" comment:"data tables, see doc/config.md for additional info"`
	Tags         Tags               `toml:"tags"          json:"tags"       comment:"is not recommended to use, https://github.com/lomik/graphite-clickhouse/wiki/TagsRU" commented:"true"`
	Carbonlink   Carbonlink         `toml:"carbonlink"    json:"carbonlink"`
	Prometheus   Prometheus         `toml:"prometheus"    json:"prometheus"`
	Debug        Debug              `toml:"debug"         json:"debug"      comment:"see doc/debugging.md"`
	Logging      []zapwriter.Config `toml:"logging"       json:"logging"`
}

// New returns *Config with default values
func New() *Config {
	cfg := &Config{
		Common: Common{
			Listen:      ":9090",
			PprofListen: "",
			// MetricPrefix: "carbon.graphite-clickhouse.{host}",
			// MetricInterval: time.Minute,
			// MetricEndpoint: MetricEndpointLocal,
			MaxCPU:                 1,
			MaxMetricsInFindAnswer: 0,
			MaxMetricsPerTarget:    15000, // This is arbitrary value to protect CH from overload
			MemoryReturnInterval:   0,
			FindCacheConfig: CacheConfig{
				Type:              "null",
				DefaultTimeoutSec: 0,
				ShortTimeoutSec:   0,
				FindTimeoutSec:    0,
			},
			DegragedMultiply: 4.0,
			DegragedLoad:     1.0,
		},
		ClickHouse: ClickHouse{
			URL:                     "http://localhost:8123?cancel_http_readonly_queries_on_client_close=1",
			DataTimeout:             time.Minute,
			ProgressSendingInterval: 10 * time.Second,
			IndexTable:              "graphite_index",
			IndexUseDaily:           true,
			TaggedUseDaily:          true,
			IndexReverse:            "auto",
			IndexReverses:           IndexReverses{},
			IndexTimeout:            time.Minute,
			TaggedTable:             "graphite_tagged",
			TaggedAutocompleDays:    7,
			ExtraPrefix:             "",
			ConnectTimeout:          time.Second,
			DataTableLegacy:         "",
			RollupConfLegacy:        "auto",
			MaxDataPoints:           1048576,
			InternalAggregation:     true,
			FindLimiter:             limiter.NoopLimiter{},
			TagsLimiter:             limiter.NoopLimiter{},
		},
		Tags: Tags{
			Threads:     1,
			Compression: "gzip",
		},
		Carbonlink: Carbonlink{
			Threads:        10,
			Retries:        2,
			ConnectTimeout: 50 * time.Millisecond,
			QueryTimeout:   50 * time.Millisecond,
			TotalTimeout:   500 * time.Millisecond,
		},
		Prometheus: Prometheus{
			ExternalURLRaw:             "",
			PageTitle:                  "Prometheus Time Series Collection and Processing Server",
			Listen:                     ":9092",
			LookbackDelta:              5 * time.Minute,
			RemoteReadConcurrencyLimit: 10,
		},
		Debug: Debug{
			Directory:        "",
			DirectoryPerm:    0755,
			ExternalDataPerm: 0,
		},
		Logging: nil,
	}

	return cfg
}

// Compile checks if IndexReverseRule are valid in the IndexReverses and compiles regexps if set
func (ir IndexReverses) Compile() error {
	var err error

	for i, n := range ir {
		if len(n.RegexStr) > 0 {
			if n.Regex, err = regexp.Compile(n.RegexStr); err != nil {
				return err
			}
		} else if len(n.Prefix) == 0 && len(n.Suffix) == 0 {
			return fmt.Errorf("empthy index-use-reverses[%d] rule", i)
		}

		if _, ok := IndexReverse[n.Reverse]; !ok {
			return fmt.Errorf("%s is not valid value for index-reverses.reverse", n.Reverse)
		}
	}

	return nil
}

func newLoggingConfig() zapwriter.Config {
	cfg := zapwriter.NewConfig()
	cfg.File = "/var/log/graphite-clickhouse/graphite-clickhouse.log"

	return cfg
}

func DefaultConfig() (*Config, error) {
	cfg := New()

	if cfg.Logging == nil {
		cfg.Logging = make([]zapwriter.Config, 0)
	}

	if len(cfg.Logging) == 0 {
		cfg.Logging = append(cfg.Logging, newLoggingConfig())
	}

	if len(cfg.DataTable) == 0 {
		interval := time.Minute
		cfg.DataTable = []DataTable{
			{
				Table:              "graphite_data",
				RollupConf:         "auto",
				RollupAutoInterval: &interval,
			},
		}
	}

	if len(cfg.ClickHouse.IndexReverses) == 0 {
		cfg.ClickHouse.IndexReverses = IndexReverses{
			&IndexReverseRule{Suffix: "suffix", Reverse: "auto"},
			&IndexReverseRule{Prefix: "prefix", Reverse: "direct"},
			&IndexReverseRule{RegexStr: "regex", Reverse: "reversed"},
		}

		err := cfg.ClickHouse.IndexReverses.Compile()
		if err != nil {
			return nil, err
		}
	}

	return cfg, nil
}

// PrintDefaultConfig prints the default config with some additions to be useful
func PrintDefaultConfig() error {
	buf := new(bytes.Buffer)

	cfg, err := DefaultConfig()
	if err != nil {
		return err
	}

	encoder := toml.NewEncoder(buf).Indentation(" ").Order(toml.OrderPreserve).CompactComments(true)

	if err := encoder.Encode(cfg); err != nil {
		return err
	}

	out := strings.Replace(buf.String(), "\n", "", 1)

	fmt.Print(out)

	return nil
}

// ReadConfig reads the content of the file with given name and process it to the *Config
func ReadConfig(filename string, exactConfig bool) (*Config, []zap.Field, error) {
	var err error

	var body []byte
	if filename != "" {
		body, err = os.ReadFile(filename)
		if err != nil {
			return nil, nil, err
		}
	}

	return Unmarshal(body, exactConfig)
}

// Unmarshal process the body to *Config
func Unmarshal(body []byte, exactConfig bool) (cfg *Config, warns []zap.Field, err error) {
	deprecations := make(map[string]error)

	cfg = New()

	if len(body) != 0 {
		// TODO: remove in v0.14
		if bytes.Index(body, []byte("\n[logging]\n")) != -1 || bytes.Index(body, []byte("[logging]")) == 0 {
			deprecations["logging"] = fmt.Errorf("single [logging] value became multivalue [[logging]]; please, adjust your config")

			body = bytes.ReplaceAll(body, []byte("\n[logging]\n"), []byte("\n[[logging]]\n"))
			if bytes.Index(body, []byte("[logging]")) == 0 {
				body = bytes.Replace(body, []byte("[logging]"), []byte("[[logging]]"), 1)
			}
		}

		decoder := toml.NewDecoder(bytes.NewReader(body))
		decoder.Strict(exactConfig)

		err := decoder.Decode(cfg)
		if err != nil {
			return nil, nil, err
		}
	}

	if cfg.Logging == nil {
		cfg.Logging = make([]zapwriter.Config, 0)
	}

	if cfg.ClickHouse.RenderConcurrentQueries > cfg.ClickHouse.RenderMaxQueries && cfg.ClickHouse.RenderMaxQueries > 0 {
		cfg.ClickHouse.RenderConcurrentQueries = 0
	}

	chURL, err := clickhouseURLValidate(cfg.ClickHouse.URL)
	if err != nil {
		return nil, nil, err
	}

	if !reflect.DeepEqual(cfg.ClickHouse.TLSParams, config.TLS{}) {
		tlsConfig, warnings, err := config.ParseClientTLSConfig(&cfg.ClickHouse.TLSParams)
		if err != nil {
			return nil, nil, err
		}

		if chURL.Scheme == "https" {
			cfg.ClickHouse.TLSConfig = tlsConfig
		} else {
			warnings = append(warnings, "TLS configurations is ignored because scheme is not HTTPS")
		}

		warns = append(warns, zap.Strings("tls-config", warnings))
	}

	for i := range cfg.ClickHouse.QueryParams {
		if cfg.ClickHouse.QueryParams[i].ConcurrentQueries > cfg.ClickHouse.QueryParams[i].MaxQueries && cfg.ClickHouse.QueryParams[i].MaxQueries > 0 {
			cfg.ClickHouse.QueryParams[i].ConcurrentQueries = 0
		}

		if cfg.ClickHouse.QueryParams[i].Duration == 0 {
			return nil, nil, fmt.Errorf("query duration param not set for: %+v", cfg.ClickHouse.QueryParams[i])
		}

		if cfg.ClickHouse.QueryParams[i].DataTimeout == 0 {
			cfg.ClickHouse.QueryParams[i].DataTimeout = cfg.ClickHouse.DataTimeout
		}

		if cfg.ClickHouse.QueryParams[i].URL == "" {
			// reuse default url
			cfg.ClickHouse.QueryParams[i].URL = cfg.ClickHouse.URL
		}

		if _, err = clickhouseURLValidate(cfg.ClickHouse.QueryParams[i].URL); err != nil {
			return nil, nil, err
		}
	}

	cfg.ClickHouse.QueryParams = append(
		[]QueryParam{{
			URL: cfg.ClickHouse.URL, DataTimeout: cfg.ClickHouse.DataTimeout,
			MaxQueries: cfg.ClickHouse.RenderMaxQueries, ConcurrentQueries: cfg.ClickHouse.RenderConcurrentQueries,
			AdaptiveQueries: cfg.ClickHouse.RenderAdaptiveQueries,
		}},
		cfg.ClickHouse.QueryParams...,
	)

	sort.SliceStable(cfg.ClickHouse.QueryParams, func(i, j int) bool {
		return cfg.ClickHouse.QueryParams[i].Duration < cfg.ClickHouse.QueryParams[j].Duration
	})

	if len(cfg.Logging) == 0 {
		cfg.Logging = append(cfg.Logging, newLoggingConfig())
	}

	if err = zapwriter.CheckConfig(cfg.Logging, nil); err != nil {
		return nil, nil, err
	}

	// Check if debug directory exists or could be created
	if cfg.Debug.Directory != "" {
		info, err := os.Stat(cfg.Debug.Directory)
		if os.IsNotExist(err) {
			err := os.MkdirAll(cfg.Debug.Directory, os.ModeDir|cfg.Debug.DirectoryPerm)
			if err != nil {
				return nil, nil, err
			}
		} else if !info.IsDir() {
			return nil, nil, fmt.Errorf("the file for external data debug dumps exists and is not a directory: %v", cfg.Debug.Directory)
		}
	}

	if _, ok := IndexReverse[cfg.ClickHouse.IndexReverse]; !ok {
		return nil, nil, fmt.Errorf("%s is not valid value for index-reverse", cfg.ClickHouse.IndexReverse)
	}

	err = cfg.ClickHouse.IndexReverses.Compile()
	if err != nil {
		return nil, nil, err
	}

	if cfg.Common.FindCache, err = CreateCache("index", &cfg.Common.FindCacheConfig); err == nil {
		if cfg.Common.FindCacheConfig.Type != "null" {
			warns = append(warns, zap.Any("enable find cache", zap.String("type", cfg.Common.FindCacheConfig.Type)))
		}
	} else {
		return nil, nil, err
	}

	l := len(cfg.Common.TargetBlacklist)
	if l > 0 {
		cfg.Common.Blacklist = make([]*regexp.Regexp, l)
		for i := 0; i < l; i++ {
			r, err := regexp.Compile(cfg.Common.TargetBlacklist[i])
			if err != nil {
				return nil, nil, err
			}

			cfg.Common.Blacklist[i] = r
		}
	}

	err = cfg.ProcessDataTables()
	if err != nil {
		return nil, nil, err
	}

	// compute prometheus external url
	rawURL := cfg.Prometheus.ExternalURLRaw
	if rawURL == "" {
		hostname, err := os.Hostname()
		if err != nil {
			return nil, nil, err
		}

		_, port, err := net.SplitHostPort(cfg.Common.Listen)
		if err != nil {
			return nil, nil, err
		}

		rawURL = fmt.Sprintf("http://%s:%s/", hostname, port)
	}

	cfg.Prometheus.ExternalURL, err = url.Parse(rawURL)
	if err != nil {
		return nil, nil, err
	}

	cfg.Prometheus.ExternalURL.Path = strings.TrimRight(cfg.Prometheus.ExternalURL.Path, "/")

	checkDeprecations(cfg, deprecations)

	if len(deprecations) != 0 {
		deprecationList := make([]error, len(deprecations))
		for name, message := range deprecations {
			deprecationList = append(deprecationList, errors.Wrap(message, name))
		}

		warns = append(warns, zap.Errors("config deprecations", deprecationList))
	}

	switch strings.ToLower(cfg.ClickHouse.DateFormat) {
	case "utc":
		date.SetUTC()
	case "both":
		date.SetBoth()
	default:
		if cfg.ClickHouse.DateFormat != "" && cfg.ClickHouse.DateFormat != "default" {
			return nil, nil, fmt.Errorf("unsupported date-format: %s", cfg.ClickHouse.DateFormat)
		}
	}

	if cfg.ClickHouse.FindConcurrentQueries > cfg.ClickHouse.FindMaxQueries && cfg.ClickHouse.FindMaxQueries > 0 {
		cfg.ClickHouse.FindConcurrentQueries = 0
	}

	if cfg.ClickHouse.TagsConcurrentQueries > cfg.ClickHouse.TagsMaxQueries && cfg.ClickHouse.TagsMaxQueries > 0 {
		cfg.ClickHouse.TagsConcurrentQueries = 0
	}

	metricsEnabled := cfg.setupGraphiteMetrics()

	cfg.ClickHouse.FindLimiter = limiter.NewALimiter(
		cfg.ClickHouse.FindMaxQueries, cfg.ClickHouse.FindConcurrentQueries, cfg.ClickHouse.FindAdaptiveQueries,
		metricsEnabled, "find", "all",
	)

	cfg.ClickHouse.TagsLimiter = limiter.NewALimiter(
		cfg.ClickHouse.TagsMaxQueries, cfg.ClickHouse.TagsConcurrentQueries, cfg.ClickHouse.TagsAdaptiveQueries,
		metricsEnabled, "tags", "all",
	)

	for i := range cfg.ClickHouse.QueryParams {
		cfg.ClickHouse.QueryParams[i].Limiter = limiter.NewALimiter(
			cfg.ClickHouse.QueryParams[i].MaxQueries, cfg.ClickHouse.QueryParams[i].ConcurrentQueries,
			cfg.ClickHouse.QueryParams[i].AdaptiveQueries,
			metricsEnabled, "render", duration.String(cfg.ClickHouse.QueryParams[i].Duration),
		)
	}

	for u, q := range cfg.ClickHouse.UserLimits {
		q.Limiter = limiter.NewALimiter(
			q.MaxQueries, q.ConcurrentQueries, q.AdaptiveQueries, metricsEnabled, u, "all",
		)
		cfg.ClickHouse.UserLimits[u] = q
	}

	return cfg, warns, nil
}

// NeedLoadAvgColect check if load avg collect is neeeded
func (c *Config) NeedLoadAvgColect() bool {
	if c.Common.SD != "" {
		if c.Common.DegragedMultiply <= 0 {
			c.Common.DegragedMultiply = 4.0
		}

		if c.Common.DegragedLoad <= 0 {
			c.Common.DegragedLoad = 1.0
		}

		if c.Common.BaseWeight <= 0 {
			c.Common.BaseWeight = 100
		}

		if c.Common.SDNamespace == "" {
			c.Common.SDNamespace = "graphite"
		}

		if c.Common.SDExpire < 24*time.Hour {
			c.Common.SDExpire = 24 * time.Hour
		}

		return true
	}

	if c.ClickHouse.RenderAdaptiveQueries > 0 {
		return true
	}

	if c.ClickHouse.FindAdaptiveQueries > 0 {
		return true
	}

	if c.ClickHouse.TagsAdaptiveQueries > 0 {
		return true
	}

	for _, u := range c.ClickHouse.UserLimits {
		if u.AdaptiveQueries > 0 {
			return true
		}
	}

	return false
}

// ProcessDataTables checks if legacy `data`-table config is used, compiles regexps for `target-match-any` and `target-match-all`
// parameters, sets the rollup configuration and proper context.
func (c *Config) ProcessDataTables() (err error) {
	if c.ClickHouse.DataTableLegacy != "" {
		c.DataTable = append(c.DataTable, DataTable{
			Table:      c.ClickHouse.DataTableLegacy,
			RollupConf: c.ClickHouse.RollupConfLegacy,
		})
	}

	for i := 0; i < len(c.DataTable); i++ {
		if c.DataTable[i].TargetMatchAny != "" {
			r, err := regexp.Compile(c.DataTable[i].TargetMatchAny)
			if err != nil {
				return err
			}

			c.DataTable[i].TargetMatchAnyRegexp = r
		}

		if c.DataTable[i].TargetMatchAll != "" {
			r, err := regexp.Compile(c.DataTable[i].TargetMatchAll)
			if err != nil {
				return err
			}

			c.DataTable[i].TargetMatchAllRegexp = r
		}

		rdp := c.DataTable[i].RollupDefaultPrecision
		rdf := c.DataTable[i].RollupDefaultFunction

		if c.DataTable[i].RollupConf == "auto" || c.DataTable[i].RollupConf == "" {
			table := c.DataTable[i].Table
			interval := time.Minute

			if c.DataTable[i].RollupAutoTable != "" {
				table = c.DataTable[i].RollupAutoTable
			}

			if c.DataTable[i].RollupAutoInterval != nil {
				interval = *c.DataTable[i].RollupAutoInterval
			}

			c.DataTable[i].Rollup, err = rollup.NewAuto(
				c.ClickHouse.URL,
				c.ClickHouse.TLSConfig,
				table,
				interval,
				rdp,
				rdf,
			)
		} else if c.DataTable[i].RollupConf == "none" {
			c.DataTable[i].Rollup, err = rollup.NewDefault(rdp, rdf)
		} else {
			c.DataTable[i].Rollup, err = rollup.NewXMLFile(c.DataTable[i].RollupConf, rdp, rdf)
		}

		if err != nil {
			return err
		}

		if len(c.DataTable[i].Context) == 0 {
			c.DataTable[i].ContextMap = knownDataTableContext
		} else {
			c.DataTable[i].ContextMap = make(map[string]bool)
			for _, ctx := range c.DataTable[i].Context {
				if !knownDataTableContext[ctx] {
					return fmt.Errorf("unknown context %#v", ctx)
				}

				c.DataTable[i].ContextMap[ctx] = true
			}
		}
	}

	return nil
}

func checkDeprecations(cfg *Config, d map[string]error) {
	if cfg.ClickHouse.DataTableLegacy != "" {
		d["data-table"] = fmt.Errorf("data-table parameter in [clickhouse] is deprecated; use [[data-table]]")
	}
}

func CreateCache(cacheName string, cacheConfig *CacheConfig) (cache.BytesCache, error) {
	if cacheConfig.DefaultTimeoutSec <= 0 && cacheConfig.ShortTimeoutSec <= 0 && cacheConfig.FindTimeoutSec <= 0 {
		return nil, nil
	}

	if cacheConfig.DefaultTimeoutSec < cacheConfig.ShortTimeoutSec {
		cacheConfig.DefaultTimeoutSec = cacheConfig.ShortTimeoutSec
	}

	if cacheConfig.ShortTimeoutSec < 0 || cacheConfig.DefaultTimeoutSec == cacheConfig.ShortTimeoutSec {
		// broken value or short timeout not need due to equal
		cacheConfig.ShortTimeoutSec = 0
	}

	if cacheConfig.DefaultTimeoutSec < cacheConfig.ShortTimeoutSec {
		cacheConfig.DefaultTimeoutSec = cacheConfig.ShortTimeoutSec
	}

	if cacheConfig.ShortDuration == 0 {
		cacheConfig.ShortDuration = 3 * time.Hour
	}

	if cacheConfig.ShortUntilOffsetSec == 0 {
		cacheConfig.ShortUntilOffsetSec = 120
	}

	cacheConfig.DefaultTimeoutStr = strconv.Itoa(int(cacheConfig.DefaultTimeoutSec))
	cacheConfig.ShortTimeoutStr = strconv.Itoa(int(cacheConfig.ShortTimeoutSec))

	switch cacheConfig.Type {
	case "memcache":
		if len(cacheConfig.MemcachedServers) == 0 {
			return nil, fmt.Errorf(cacheName + ": memcache cache requested but no memcache servers provided")
		}

		return cache.NewMemcached("gch-"+cacheName, cacheConfig.MemcachedServers...), nil
	case "mem":
		return cache.NewExpireCache(uint64(cacheConfig.Size * 1024 * 1024)), nil
	case "null":
		// defaults
		return nil, nil
	default:
		return nil, fmt.Errorf(
			"%s: unknown cache type '%s', known_cache_types 'null', 'mem', 'memcache'",
			cacheName,
			cacheConfig.Type,
		)
	}
}

func (c *Config) setupGraphiteMetrics() bool {
	if c.Metrics.MetricEndpoint == "" {
		metrics.DisableMetrics()
	} else {
		if c.Metrics.MetricInterval == 0 {
			c.Metrics.MetricInterval = 60 * time.Second
		}

		if c.Metrics.MetricTimeout == 0 {
			c.Metrics.MetricTimeout = time.Second
		}

		hostname, _ := os.Hostname()
		fqdn := strings.ReplaceAll(hostname, ".", "_")
		hostname = strings.Split(hostname, ".")[0]

		c.Metrics.MetricPrefix = strings.ReplaceAll(c.Metrics.MetricPrefix, "{prefix}", c.Metrics.MetricPrefix)
		c.Metrics.MetricPrefix = strings.ReplaceAll(c.Metrics.MetricPrefix, "{fqdn}", fqdn)
		c.Metrics.MetricPrefix = strings.ReplaceAll(c.Metrics.MetricPrefix, "{host}", hostname)

		// register our metrics with graphite
		metrics.Graphite = graphite.New(c.Metrics.MetricInterval, c.Metrics.MetricPrefix, c.Metrics.MetricEndpoint, c.Metrics.MetricTimeout)

		if c.Metrics.Statsd != "" && c.Metrics.ExtendedStat {
			var err error

			config := &statsd.ClientConfig{
				Address:       c.Metrics.Statsd,
				Prefix:        c.Metrics.MetricPrefix,
				ResInterval:   5 * time.Minute,
				UseBuffered:   true,
				FlushInterval: 300 * time.Millisecond,
			}

			metrics.Gstatsd, err = statsd.NewClientWithConfig(config)
			if err != nil {
				metrics.Gstatsd = metrics.NullSender{}

				fmt.Fprintf(os.Stderr, "statsd init: %v\n", err)
			}
		}

		metrics.InitMetrics(&c.Metrics, c.ClickHouse.FindMaxQueries > 0, c.ClickHouse.TagsMaxQueries > 0)
	}

	metrics.AutocompleteQMetric = metrics.InitQueryMetrics("tags", &c.Metrics)
	metrics.FindQMetric = metrics.InitQueryMetrics("find", &c.Metrics)

	for i := 0; i < len(c.DataTable); i++ {
		c.DataTable[i].QueryMetrics = metrics.InitQueryMetrics(c.DataTable[i].Table, &c.Metrics)
	}

	if c.ClickHouse.IndexTable != "" {
		metrics.InitQueryMetrics(c.ClickHouse.IndexTable, &c.Metrics)
	}

	if c.ClickHouse.TaggedTable != "" {
		metrics.InitQueryMetrics(c.ClickHouse.TaggedTable, &c.Metrics)
	}

	if c.ClickHouse.TagsCountTable != "" {
		metrics.InitQueryMetrics(c.ClickHouse.TagsCountTable, &c.Metrics)
	}

	return metrics.Graphite != nil
}

func (c *Config) GetUserFindLimiter(username string) limiter.ServerLimiter {
	if username != "" && len(c.ClickHouse.UserLimits) > 0 {
		if q, ok := c.ClickHouse.UserLimits[username]; ok {
			return q.Limiter
		}
	}

	return c.ClickHouse.FindLimiter
}

func (c *Config) GetUserTagsLimiter(username string) limiter.ServerLimiter {
	if username != "" && len(c.ClickHouse.UserLimits) > 0 {
		if q, ok := c.ClickHouse.UserLimits[username]; ok {
			return q.Limiter
		}
	}

	return c.ClickHouse.TagsLimiter
}

// search on sorted slice
func GetQueryParam(a []QueryParam, duration time.Duration) int {
	if indx := binarySearchQueryParamLe(a, duration, 0, len(a)); indx == -1 {
		return 0
	} else {
		return indx
	}
}


================================================
FILE: config/config_test.go
================================================
package config

import (
	"fmt"
	"io/fs"
	"math"
	"net/url"
	"os"
	"regexp"
	"regexp/syntax"
	"syscall"
	"testing"
	"time"

	"github.com/lomik/zapwriter"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/lomik/graphite-clickhouse/limiter"
	"github.com/lomik/graphite-clickhouse/metrics"
)

func TestProcessDataTables(t *testing.T) {
	type in struct {
		table       DataTable
		tableLegacy string
	}

	type out struct {
		tables []DataTable
		err    error
	}

	type ctx map[string]bool

	regexpCompileWrapper := func(re string) *regexp.Regexp {
		r, _ := regexp.Compile(re)
		return r
	}

	tests := []struct {
		name string
		in   in
		out  out
	}{
		{
			name: "legacy table only",
			in: in{
				tableLegacy: "graphite.data",
			},
			out: out{
				[]DataTable{
					{
						Table:      "graphite.data",
						RollupConf: "auto",
						ContextMap: ctx{"graphite": true, "prometheus": true},
					},
				},
				nil,
			},
		},
		{
			name: "legacy and normal tables",
			in: in{
				table:       DataTable{Table: "graphite.new_data"},
				tableLegacy: "graphite.data",
			},
			out: out{
				[]DataTable{
					{
						Table:      "graphite.new_data",
						ContextMap: ctx{"graphite": true, "prometheus": true},
					},
					{
						Table:      "graphite.data",
						RollupConf: "auto",
						ContextMap: ctx{"graphite": true, "prometheus": true},
					},
				},
				nil,
			},
		},
		{
			name: "fail to compile TargetMatchAll",
			in: in{
				table: DataTable{Table: "graphite.data", TargetMatchAll: "[2223"},
			},
			out: out{
				[]DataTable{{Table: "graphite.data", TargetMatchAll: "[2223"}},
				&syntax.Error{Code: syntax.ErrMissingBracket, Expr: "[2223"},
			},
		},
		{
			name: "fail to compile TargetMatchAny",
			in: in{
				table: DataTable{Table: "graphite.data", TargetMatchAny: "[2223"},
			},
			out: out{
				[]DataTable{{Table: "graphite.data", TargetMatchAny: "[2223"}},
				&syntax.Error{Code: syntax.ErrMissingBracket, Expr: "[2223"},
			},
		},
		{
			name: "fail to compile TargetMatchAny",
			in: in{
				table: DataTable{Table: "graphite.data", TargetMatchAny: "[2223"},
			},
			out: out{
				[]DataTable{{Table: "graphite.data", TargetMatchAny: "[2223"}},
				&syntax.Error{Code: syntax.ErrMissingBracket, Expr: "[2223"},
			},
		},
		{
			name: "fail to read xml rollup",
			in: in{
				table: DataTable{Table: "graphite.data", RollupConf: "/some/file/that/does/not/hopefully/exists/on/the/disk"},
			},
			out: out{
				[]DataTable{{Table: "graphite.data", RollupConf: "/some/file/that/does/not/hopefully/exists/on/the/disk"}},
				&fs.PathError{Op: "open", Path: "/some/file/that/does/not/hopefully/exists/on/the/disk", Err: syscall.ENOENT},
			},
		},
		{
			name: "unknown context",
			in: in{
				table: DataTable{Table: "graphite.data", Context: []string{"unexpected"}},
			},
			out: out{
				[]DataTable{
					{
						Table:      "graphite.data",
						Context:    []string{"unexpected"},
						ContextMap: ctx{},
					},
				},
				fmt.Errorf("unknown context \"unexpected\""),
			},
		},
		{
			name: "check all works",
			in: in{
				table: DataTable{
					Table:                  "graphite.data",
					Reverse:                true,
					TargetMatchAll:         "^.*[asdf][.].*",
					TargetMatchAny:         "^.*{a|s|d|f}[.].*",
					RollupConf:             "none",
					RollupDefaultFunction:  "any",
					RollupDefaultPrecision: 61,
					RollupUseReverted:      true,
					Context:                []string{"prometheus"},
				},
				tableLegacy: "table",
			},
			out: out{
				[]DataTable{
					{
						Table:                  "graphite.data",
						Reverse:                true,
						TargetMatchAll:         "^.*[asdf][.].*",
						TargetMatchAny:         "^.*{a|s|d|f}[.].*",
						TargetMatchAllRegexp:   regexpCompileWrapper("^.*[asdf][.].*"),
						TargetMatchAnyRegexp:   regexpCompileWrapper("^.*{a|s|d|f}[.].*"),
						RollupConf:             "none",
						RollupDefaultFunction:  "any",
						RollupDefaultPrecision: 61,
						RollupUseReverted:      true,
						Context:                []string{"prometheus"},
						ContextMap:             ctx{"prometheus": true},
					},
					{
						Table:      "table",
						RollupConf: "auto",
						ContextMap: ctx{"graphite": true, "prometheus": true},
					},
				},
				nil,
			},
		},
		{
			name: "unknown context",
			in: in{
				table: DataTable{Table: "graphite.data", Context: []string{"unexpected"}},
			},
			out: out{
				[]DataTable{
					{
						Table:      "graphite.data",
						Context:    []string{"unexpected"},
						ContextMap: ctx{},
					},
				},
				fmt.Errorf("unknown context \"unexpected\""),
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			cfg := New()
			if test.in.table.Table != "" {
				cfg.DataTable = []DataTable{test.in.table}
			}

			if test.in.tableLegacy != "" {
				cfg.ClickHouse.DataTableLegacy = test.in.tableLegacy
			}

			err := cfg.ProcessDataTables()
			if err != nil {
				assert.Equal(t, test.out.err, err)
				return
			}

			assert.Equal(t, len(test.out.tables), len(cfg.DataTable))
			// it's difficult to check rollup.Rollup because Rules.updated field
			// We explicitly don't check it here
			for i := range cfg.DataTable {
				test.out.tables[i].Rollup = nil
				cfg.DataTable[i].Rollup = nil
			}

			assert.Equal(t, test.out.tables, cfg.DataTable)
		})
	}
}

func TestKnownDataTableContext(t *testing.T) {
	assert.Equal(t, map[string]bool{ContextGraphite: true, ContextPrometheus: true}, knownDataTableContext)
}

func TestReadConfig(t *testing.T) {
	body := []byte(
		`[common]
listen = "[::1]:9090"
pprof-listen = "127.0.0.1:9091"
max-cpu = 15
max-metrics-in-find-answer = 13
max-metrics-per-target = 16
target-blacklist = ['^blacklisted']
memory-return-interval = "12s150ms"


[clickhouse]
url = "http://somehost:8123"
index-table = "graphite_index"
index-use-daily = false
index-reverse = "direct"
index-reverses = [
  {suffix = "suf", prefix = "pref", reverse = "direct"},
  {regex = "^reg$", reverse = "reversed"},
]
tagged-table = "graphite_tags"
tagged-autocomplete-days = 5
tagged-use-daily = false
tree-table = "tree"
reverse-tree-table = "reversed_tree"
date-tree-table = "data_tree"
date-tree-table-version = 2
tag-table = "tag_table"
extra-prefix = "tum.pu-dum"
data-table = "data"
rollup-conf = "none"
max-data-points = 8000
internal-aggregation = true
data-timeout = "64s"
index-timeout = "4s"
tree-timeout = "5s"
connect-timeout = "2s"

# DataTable is tested in TestProcessDataTables
# [[data-table]]
# table = "another_data"
# rollup-conf = "auto"
# rollup-conf-table = "another_table"

[tags]
rules = "filename"
date = "2012-12-12"
extra-where = "AND case"
input-file = "input"
output-file = "output"
threads = 5
compression = "zstd"
version = 42
select-chunks-count = 15

[carbonlink]
server = "server:3333"
threads-per-request = 5
connect-timeout = "250ms"
query-timeout = "350ms"
total-timeout = "800ms"

[prometheus]
listen = ":9092"
external-url = "https://server:3456/uri"
page-title = "Prometheus Time Series"
lookback-delta = "5m"

[debug]
directory = "tests_tmp"
directory-perm = 0o755
external-data-perm = 0o640

[[logging]]
logger = "debugger"
file = "stdout"
level = "debug"
encoding = "console"
encoding-time = "iso8601"
encoding-duration = "string"
sample-tick = "5ms"
sample-initial = 1
sample-thereafter = 2

[[logging]]
logger = "logger"
file = "tests_tmp/logger.txt"
level = "info"
encoding = "json"
encoding-time = "epoch"
encoding-duration = "seconds"
sample-tick = "50ms"
sample-initial = 10
sample-thereafter = 12
`,
	)
	config, _, err := Unmarshal(body, false)
	expected := New()

	require.NoError(t, err)

	// Common
	expected.Common = Common{
		Listen:                 "[::1]:9090",
		PprofListen:            "127.0.0.1:9091",
		MaxCPU:                 15,
		MaxMetricsInFindAnswer: 13,
		MaxMetricsPerTarget:    16,
		TargetBlacklist:        []string{"^blacklisted"},
		Blacklist:              make([]*regexp.Regexp, 1),
		MemoryReturnInterval:   12150000000,
		FindCacheConfig: CacheConfig{
			Type:              "null",
			DefaultTimeoutSec: 0,
			ShortTimeoutSec:   0,
		},
		DegragedMultiply: 4.0,
		DegragedLoad:     1.0,
	}
	expe
Download .txt
gitextract_n919tixz/

├── .gitattributes
├── .github/
│   └── workflows/
│       ├── codeql.yml
│       ├── docker.yml
│       ├── lint.yml
│       ├── release.yml
│       ├── tests-sd.yml
│       └── tests.yml
├── .gitignore
├── .golangci.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── autocomplete/
│   ├── autocomplete.go
│   └── autocomplete_test.go
├── cache/
│   └── cache.go
├── capabilities/
│   └── handler.go
├── cmd/
│   ├── e2e-test/
│   │   ├── carbon-clickhouse.go
│   │   ├── checks.go
│   │   ├── clickhouse.go
│   │   ├── container.go
│   │   ├── e2etesting.go
│   │   ├── errors.go
│   │   ├── graphite-clickhouse.go
│   │   ├── main.go
│   │   ├── rproxy.go
│   │   └── utils.go
│   └── graphite-clickhouse-client/
│       └── main.go
├── config/
│   ├── .gitignore
│   ├── config.go
│   ├── config_test.go
│   ├── json.go
│   └── json_test.go
├── deploy/
│   ├── doc/
│   │   ├── .gitignore
│   │   └── config.md
│   └── root/
│       └── usr/
│           └── lib/
│               └── systemd/
│                   └── system/
│                       └── graphite-clickhouse.service
├── doc/
│   ├── aggregation.md
│   ├── config.md
│   ├── debugging.md
│   ├── graphite_clickhouse.gliffy
│   ├── index-table.md
│   └── release.md
├── find/
│   ├── find.go
│   ├── handler.go
│   ├── handler_json_test.go
│   └── handler_test.go
├── finder/
│   ├── base.go
│   ├── blacklist.go
│   ├── date.go
│   ├── date_reverse.go
│   ├── date_reverse_test.go
│   ├── finder.go
│   ├── index.go
│   ├── index_test.go
│   ├── mock.go
│   ├── plain_from_tagged.go
│   ├── plain_from_tagged_test.go
│   ├── prefix.go
│   ├── prefix_test.go
│   ├── reverse.go
│   ├── reverse_test.go
│   ├── split.go
│   ├── split_test.go
│   ├── tag.go
│   ├── tag_test.go
│   ├── tagged.go
│   ├── tagged_test.go
│   ├── tags_count_querier.go
│   └── unescape.go
├── go.mod
├── go.sum
├── graphite-clickhouse.go
├── healthcheck/
│   └── healthcheck.go
├── helper/
│   ├── RowBinary/
│   │   └── encode.go
│   ├── clickhouse/
│   │   ├── clickhouse.go
│   │   ├── clickhouse_test.go
│   │   ├── external-data.go
│   │   └── external-data_test.go
│   ├── client/
│   │   ├── datetime.go
│   │   ├── errros.go
│   │   ├── find.go
│   │   ├── render.go
│   │   ├── requests.go
│   │   ├── tags.go
│   │   └── types.go
│   ├── date/
│   │   ├── date.go
│   │   └── date_test.go
│   ├── datetime/
│   │   ├── datetime.go
│   │   └── datetime_test.go
│   ├── errs/
│   │   └── errors.go
│   ├── headers/
│   │   └── headers.go
│   ├── http/
│   │   └── live-http-client.go
│   ├── pickle/
│   │   └── pickle.go
│   ├── point/
│   │   ├── func.go
│   │   ├── func_test.go
│   │   ├── point.go
│   │   └── points.go
│   ├── rollup/
│   │   ├── aggr.go
│   │   ├── compact.go
│   │   ├── compact_test.go
│   │   ├── remote.go
│   │   ├── remote_test.go
│   │   ├── rollup.go
│   │   ├── rules.go
│   │   ├── rules_test.go
│   │   ├── xml.go
│   │   └── xml_test.go
│   ├── tests/
│   │   ├── clickhouse/
│   │   │   └── server.go
│   │   └── compare/
│   │       ├── compare.go
│   │       └── expand/
│   │           └── expand.go
│   └── utils/
│       ├── utils.go
│       └── utils_test.go
├── index/
│   ├── handler.go
│   ├── index.go
│   └── index_test.go
├── issues/
│   └── daytime/
│       ├── carbon-clickhouse.conf.tpl
│       ├── graphite-clickhouse-internal-aggr.conf.tpl
│       ├── graphite-clickhouse.conf.tpl
│       └── test.toml
├── limiter/
│   ├── alimiter.go
│   ├── alimiter_test.go
│   ├── interface.go
│   ├── limiter.go
│   ├── noop.go
│   └── wlimiter.go
├── load_avg/
│   ├── load_avg.go
│   ├── load_avg_default.go
│   ├── load_avg_linux.go
│   └── load_avg_test.go
├── logs/
│   └── logger.go
├── metrics/
│   ├── limiter_metrics.go
│   ├── metrics.go
│   ├── metrics_test.go
│   ├── query_metrics.go
│   └── statsd.go
├── nfpm.yaml
├── packages.sh
├── pkg/
│   ├── alias/
│   │   ├── map.go
│   │   ├── map_tagged_test.go
│   │   └── map_test.go
│   ├── dry/
│   │   ├── math.go
│   │   ├── math_test.go
│   │   ├── strings.go
│   │   ├── strings_test.go
│   │   ├── unsafe.go
│   │   └── unsafe_test.go
│   ├── reverse/
│   │   ├── reverse.go
│   │   └── reverse_test.go
│   ├── scope/
│   │   ├── context.go
│   │   ├── http_request.go
│   │   ├── key.go
│   │   ├── logger.go
│   │   └── version.go
│   └── where/
│       ├── match.go
│       ├── match_test.go
│       ├── where.go
│       └── where_test.go
├── prometheus/
│   ├── .gitignore
│   ├── empty_iterator.go
│   ├── exemplar.go
│   ├── gatherer.go
│   ├── labels.go
│   ├── labels_test.go
│   ├── local_storage.go
│   ├── logger.go
│   ├── matcher.go
│   ├── metrics_set.go
│   ├── querier.go
│   ├── querier_select.go
│   ├── querier_select_test.go
│   ├── run.go
│   ├── run_dummy.go
│   ├── series_set.go
│   └── storage.go
├── render/
│   ├── data/
│   │   ├── carbonlink.go
│   │   ├── carbonlink_test.go
│   │   ├── ch_response.go
│   │   ├── common_step.go
│   │   ├── common_step_test.go
│   │   ├── data.go
│   │   ├── data_parse_test.go
│   │   ├── multi_target.go
│   │   ├── multi_target_test.go
│   │   ├── query.go
│   │   ├── query_test.go
│   │   ├── targets.go
│   │   └── targets_test.go
│   ├── handler.go
│   ├── handler_test.go
│   └── reply/
│       ├── formatter.go
│       ├── formatter_test.go
│       ├── json.go
│       ├── pickle.go
│       ├── protobuf.go
│       ├── protobuf_test.go
│       ├── v2_pb.go
│       ├── v2_pb_test.go
│       ├── v3_pb.go
│       └── v3_pb_test.go
├── sd/
│   ├── nginx/
│   │   ├── nginx.go
│   │   ├── nginx_test.go
│   │   └── tests/
│   │       └── nginx_cleanup_test.go
│   ├── register.go
│   └── utils/
│       └── utils.go
├── tagger/
│   ├── metric.go
│   ├── rule.go
│   ├── rule_test.go
│   ├── set.go
│   ├── tagger.go
│   ├── tagger_test.go
│   └── tree.go
└── tests/
    ├── agg_internal/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr.conf.tpl
    │   └── test.toml
    ├── agg_latest/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── agg_merge/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── agg_oneblock/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── clickhouse/
    │   ├── rollup/
    │   │   ├── config.xml
    │   │   ├── init.sql
    │   │   ├── rollup.xml
    │   │   └── users.xml
    │   └── rollup_tls/
    │       ├── config.xml
    │       ├── init.sql
    │       ├── rollup.xml
    │       ├── rootCA.crt
    │       ├── server.crt
    │       ├── server.key
    │       └── users.xml
    ├── consolidateBy/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── consul.sh
    ├── emptyseries_append/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── emptyseries_noappend/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── error_handling/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── feature_flags_both_true/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── feature_flags_dont_match_missing_tags/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── feature_flags_false/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── feature_flags_use_carbon_behaviour/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── find_cache/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-cached.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr-cached.conf.tpl
    │   └── test.toml
    ├── limitera/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr-cached.conf.tpl
    │   └── test.toml
    ├── limitermax/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr-cached.conf.tpl
    │   └── test.toml
    ├── limiterw/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr-cached.conf.tpl
    │   └── test.toml
    ├── limiterwn/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr-cached.conf.tpl
    │   └── test.toml
    ├── one_table/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse-internal-aggr.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── tags_min_in_query/
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    ├── tls/
    │   ├── ca.crt
    │   ├── carbon-clickhouse.conf.tpl
    │   ├── client.crt
    │   ├── client.key
    │   ├── graphite-clickhouse.conf.tpl
    │   └── test.toml
    └── wildcard_min_distance/
        ├── carbon-clickhouse.conf.tpl
        ├── graphite-clickhouse.conf.tpl
        └── test.toml
Download .txt
SYMBOL INDEX (1236 symbols across 178 files)

FILE: autocomplete/autocomplete.go
  type Handler (line 31) | type Handler struct
    method ServeHTTP (line 60) | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    method requestExpr (line 90) | func (h *Handler) requestExpr(r *http.Request, tcq *finder.TagCountQue...
    method ServeTags (line 232) | func (h *Handler) ServeTags(w http.ResponseWriter, r *http.Request) {
    method ServeValues (line 517) | func (h *Handler) ServeValues(w http.ResponseWriter, r *http.Request) {
  function NewTags (line 36) | func NewTags(config *config.Config) *Handler {
  function NewValues (line 44) | func NewValues(config *config.Config) *Handler {
  function dateString (line 53) | func dateString(autocompleteDays int, tm time.Time) (string, string) {
  function getTagCountQuerier (line 74) | func getTagCountQuerier(config *config.Config, opts clickhouse.Options) ...
  function taggedKey (line 142) | func taggedKey(typ string, truncateSec int32, fromDate, untilDate string...
  function taggedValuesKey (line 181) | func taggedValuesKey(typ string, truncateSec int32, fromDate, untilDate ...

FILE: autocomplete/autocomplete_test.go
  function NewRequest (line 18) | func NewRequest(method, url string, body io.Reader) *http.Request {
  type testStruct (line 24) | type testStruct struct
  function testResponce (line 31) | func testResponce(t *testing.T, step int, h *Handler, tt *testStruct, wa...
  function TestHandler_ServeTags (line 53) | func TestHandler_ServeTags(t *testing.T) {
  function TestHandler_ServeTagsWithCache (line 147) | func TestHandler_ServeTagsWithCache(t *testing.T) {
  function TestHandler_ServeValues (line 209) | func TestHandler_ServeValues(t *testing.T) {
  function TestHandler_ServeValuesWithValuePrefix (line 262) | func TestHandler_ServeValuesWithValuePrefix(t *testing.T) {
  function TestHandler_ServeValuesNameTag (line 298) | func TestHandler_ServeValuesNameTag(t *testing.T) {
  function TestHandler_ServeValuesWithCache (line 334) | func TestHandler_ServeValuesWithCache(t *testing.T) {
  function TestHandler_ServeValuesWithCacheAndExpr (line 396) | func TestHandler_ServeValuesWithCacheAndExpr(t *testing.T) {
  function TestHandler_ServeValuesWithInvalidLimit (line 469) | func TestHandler_ServeValuesWithInvalidLimit(t *testing.T) {
  function TestHandler_ServeValuesWithMultipleExpr (line 494) | func TestHandler_ServeValuesWithMultipleExpr(t *testing.T) {
  function TestHandler_ServeValuesNoCache (line 531) | func TestHandler_ServeValuesNoCache(t *testing.T) {
  function TestHandler_ServeValuesEmptyResult (line 586) | func TestHandler_ServeValuesEmptyResult(t *testing.T) {
  function TestHandler_ServeTagsWithCostOptimization (line 622) | func TestHandler_ServeTagsWithCostOptimization(t *testing.T) {
  function TestHandler_ServeValuesWithCostOptimization (line 673) | func TestHandler_ServeValuesWithCostOptimization(t *testing.T) {
  function TestHandler_ServeTagsWithWildcardExpressions (line 724) | func TestHandler_ServeTagsWithWildcardExpressions(t *testing.T) {
  function TestHandler_ServeValuesWithNoEqualityTerms (line 762) | func TestHandler_ServeValuesWithNoEqualityTerms(t *testing.T) {
  function TestHandler_ServeTagsWithHighCostTags (line 801) | func TestHandler_ServeTagsWithHighCostTags(t *testing.T) {
  function TestHandler_ServeValuesWithMixedOperators (line 851) | func TestHandler_ServeValuesWithMixedOperators(t *testing.T) {

FILE: cache/cache.go
  type BytesCache (line 20) | type BytesCache interface
  function NewExpireCache (line 25) | func NewExpireCache(maxsize uint64) BytesCache {
  type ExpireCache (line 32) | type ExpireCache struct
    method Get (line 36) | func (ec ExpireCache) Get(k string) ([]byte, error) {
    method Set (line 46) | func (ec ExpireCache) Set(k string, v []byte, expire int32) {
  function NewMemcached (line 50) | func NewMemcached(prefix string, servers ...string) BytesCache {
  type MemcachedCache (line 54) | type MemcachedCache struct
    method Get (line 60) | func (m *MemcachedCache) Get(k string) ([]byte, error) {
    method Set (line 95) | func (m *MemcachedCache) Set(k string, v []byte, expire int32) {
    method Timeouts (line 104) | func (m *MemcachedCache) Timeouts() uint64 {

FILE: capabilities/handler.go
  type Handler (line 16) | type Handler struct
    method ServeHTTP (line 26) | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  function NewHandler (line 20) | func NewHandler(config *config.Config) *Handler {

FILE: cmd/e2e-test/carbon-clickhouse.go
  type CarbonClickhouse (line 15) | type CarbonClickhouse struct
    method Start (line 29) | func (c *CarbonClickhouse) Start(testDir, clickhouseURL string) (strin...
    method Stop (line 117) | func (c *CarbonClickhouse) Stop(delete bool) (string, error) {
    method Delete (line 134) | func (c *CarbonClickhouse) Delete() (string, error) {
    method Cleanup (line 153) | func (c *CarbonClickhouse) Cleanup() {
    method Address (line 160) | func (c *CarbonClickhouse) Address() string {
    method Container (line 164) | func (c *CarbonClickhouse) Container() string {

FILE: cmd/e2e-test/checks.go
  function isFindCached (line 20) | func isFindCached(header http.Header) (string, bool) {
  function requestId (line 33) | func requestId(header http.Header) string {
  function compareFindMatch (line 46) | func compareFindMatch(errors *[]string, name, url string, actual, expect...
  function verifyMetricsFind (line 74) | func verifyMetricsFind(ch *Clickhouse, gch *GraphiteClickhouse, check *M...
  function compareTags (line 130) | func compareTags(errors *[]string, name, url string, actual, expected []...
  function verifyTags (line 158) | func verifyTags(ch *Clickhouse, gch *GraphiteClickhouse, check *TagsChec...
  function compareRender (line 234) | func compareRender(errors *[]string, name, url string, actual, expected ...
  function parseFilteringFunctions (line 314) | func parseFilteringFunctions(strFilteringFuncs []string) ([]*carbonapi_v...
  function verifyRender (line 337) | func verifyRender(ch *Clickhouse, gch *GraphiteClickhouse, check *Render...
  function debug (line 407) | func debug(test *TestSchema, ch *Clickhouse, gch *GraphiteClickhouse) {

FILE: cmd/e2e-test/clickhouse.go
  type Clickhouse (line 23) | type Clickhouse struct
    method CheckConfig (line 39) | func (c *Clickhouse) CheckConfig(rootDir string) error {
    method Key (line 72) | func (c *Clickhouse) Key() string {
    method Start (line 76) | func (c *Clickhouse) Start() (string, error) {
    method Stop (line 131) | func (c *Clickhouse) Stop(delete bool) (string, error) {
    method Delete (line 148) | func (c *Clickhouse) Delete() (string, error) {
    method URL (line 165) | func (c *Clickhouse) URL() string {
    method TLSURL (line 169) | func (c *Clickhouse) TLSURL() string {
    method Container (line 173) | func (c *Clickhouse) Container() string {
    method Exec (line 177) | func (c *Clickhouse) Exec(sql string) (bool, string) {
    method Query (line 181) | func (c *Clickhouse) Query(sql string) (string, error) {
    method Alive (line 207) | func (c *Clickhouse) Alive() bool {
    method CopyLog (line 222) | func (c *Clickhouse) CopyLog(destDir string, tail uint64) error {
    method CopyErrLog (line 246) | func (c *Clickhouse) CopyErrLog(destDir string, tail uint64) error {

FILE: cmd/e2e-test/container.go
  function imageDelete (line 13) | func imageDelete(image, version string) (bool, string) {
  function containerExist (line 31) | func containerExist(name string) (bool, string) {
  function containerRemove (line 49) | func containerRemove(name string) (bool, string) {
  function containerExec (line 67) | func containerExec(name string, args []string) (bool, string) {

FILE: cmd/e2e-test/e2etesting.go
  type Point (line 32) | type Point struct
  type InputMetric (line 40) | type InputMetric struct
  type Metric (line 46) | type Metric struct
  type RenderCheck (line 61) | type RenderCheck struct
  type MetricsFindCheck (line 89) | type MetricsFindCheck struct
  type TagsCheck (line 113) | type TagsCheck struct
  type TestSchema (line 139) | type TestSchema struct
    method HasTLSSettings (line 158) | func (schema *TestSchema) HasTLSSettings() bool {
  function getFreeTCPPort (line 162) | func getFreeTCPPort(name string) (string, error) {
  function sendPlain (line 184) | func sendPlain(network, address string, metrics []InputMetric) error {
  function verifyGraphiteClickhouse (line 219) | func verifyGraphiteClickhouse(test *TestSchema, gch *GraphiteClickhouse,...
  function testGraphiteClickhouse (line 449) | func testGraphiteClickhouse(test *TestSchema, clickhouse *Clickhouse, te...
  function runTest (line 561) | func runTest(cfg *MainConfig, clickhouse *Clickhouse, rootDir string, no...
  function clickhouseStart (line 591) | func clickhouseStart(clickhouse *Clickhouse, logger *zap.Logger) bool {
  function clickhouseStop (line 608) | func clickhouseStop(clickhouse *Clickhouse, logger *zap.Logger) (result ...
  function initTest (line 632) | func initTest(cfg *MainConfig, rootDir string, now time.Time, verbose, b...
  function loadConfig (line 886) | func loadConfig(config string, rootDir string) (*MainConfig, error) {

FILE: cmd/e2e-test/graphite-clickhouse.go
  type GraphiteClickhouse (line 20) | type GraphiteClickhouse struct
    method Start (line 33) | func (c *GraphiteClickhouse) Start(testDir, chURL, chProxyURL, chTLSUR...
    method Alive (line 119) | func (c *GraphiteClickhouse) Alive() bool {
    method Stop (line 129) | func (c *GraphiteClickhouse) Stop(cleanup bool) error {
    method Cleanup (line 155) | func (c *GraphiteClickhouse) Cleanup() {
    method URL (line 163) | func (c *GraphiteClickhouse) URL() string {
    method Cmd (line 167) | func (c *GraphiteClickhouse) Cmd() string {
    method Grep (line 171) | func (c *GraphiteClickhouse) Grep(s string) {

FILE: cmd/e2e-test/main.go
  type MainConfig (line 14) | type MainConfig struct
  function IsDir (line 18) | func IsDir(filename string) (bool, error) {
  function expandDir (line 29) | func expandDir(dirname string, paths *[]string) error {
  function expandFilename (line 51) | func expandFilename(filename string, paths *[]string) error {
  function main (line 70) | func main() {

FILE: cmd/e2e-test/rproxy.go
  type AtomicDuration (line 16) | type AtomicDuration struct
    method Store (line 20) | func (d *AtomicDuration) Store(duration time.Duration) {
    method Load (line 24) | func (d *AtomicDuration) Load() time.Duration {
    method MarshalText (line 28) | func (d *AtomicDuration) MarshalText() ([]byte, error) {
    method UnmarshalText (line 33) | func (d *AtomicDuration) UnmarshalText(b []byte) error {
  type HttpReverseProxy (line 44) | type HttpReverseProxy struct
    method Start (line 53) | func (p *HttpReverseProxy) Start(remoteURL string) (err error) {
    method Stop (line 81) | func (p *HttpReverseProxy) Stop() {
    method URL (line 92) | func (p *HttpReverseProxy) URL() string {
    method SetDelay (line 96) | func (p *HttpReverseProxy) SetDelay(delay time.Duration) {
    method GetDelay (line 100) | func (p *HttpReverseProxy) GetDelay() time.Duration {
    method SetBreakStatusCode (line 104) | func (p *HttpReverseProxy) SetBreakStatusCode(statusCode int) {
    method GetBreakStatusCode (line 108) | func (p *HttpReverseProxy) GetBreakStatusCode() int {
    method ServeHTTP (line 112) | func (p *HttpReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Re...

FILE: cmd/e2e-test/utils.go
  function cmdExec (line 5) | func cmdExec(programm string, args ...string) (string, error) {

FILE: cmd/graphite-clickhouse-client/main.go
  type StringSlice (line 17) | type StringSlice
    method Set (line 19) | func (u *StringSlice) Set(value string) error {
    method String (line 24) | func (u *StringSlice) String() string {
    method Type (line 28) | func (u *StringSlice) Type() string {
  function main (line 32) | func main() {

FILE: config/config.go
  type SDType (line 35) | type SDType
    method Set (line 44) | func (a *SDType) Set(value string) error {
    method UnmarshalText (line 57) | func (a *SDType) UnmarshalText(data []byte) error {
    method MarshalText (line 61) | func (a *SDType) MarshalText() ([]byte, error) {
    method UnmarshalJSON (line 65) | func (a *SDType) UnmarshalJSON(data []byte) error {
    method MarshalJSON (line 69) | func (a *SDType) MarshalJSON() ([]byte, error) {
    method String (line 73) | func (a *SDType) String() string {
    method Type (line 77) | func (a *SDType) Type() string {
  constant SDNone (line 38) | SDNone  SDType = iota
  constant SDNginx (line 39) | SDNginx
  type CacheConfig (line 82) | type CacheConfig struct
  type Common (line 96) | type Common struct
  type FeatureFlags (line 123) | type FeatureFlags struct
  type IndexReverseRule (line 130) | type IndexReverseRule struct
  type Costs (line 138) | type Costs struct
  type IndexReverses (line 144) | type IndexReverses
    method Compile (line 446) | func (ir IndexReverses) Compile() error {
  constant IndexAuto (line 147) | IndexAuto     = iota
  constant IndexDirect (line 148) | IndexDirect   = iota
  constant IndexReversed (line 149) | IndexReversed = iota
  type UserLimits (line 162) | type UserLimits struct
  type QueryParam (line 170) | type QueryParam struct
  function binarySearchQueryParamLe (line 182) | func binarySearchQueryParamLe(a []QueryParam, duration time.Duration, st...
  type ClickHouse (line 209) | type ClickHouse struct
  function clickhouseURLValidate (line 268) | func clickhouseURLValidate(chURL string) (*url.URL, error) {
  type Tags (line 282) | type Tags struct
  type Carbonlink (line 295) | type Carbonlink struct
  type Prometheus (line 305) | type Prometheus struct
  constant ContextGraphite (line 316) | ContextGraphite = "graphite"
  constant ContextPrometheus (line 318) | ContextPrometheus = "prometheus"
  type DataTable (line 327) | type DataTable struct
  type Debug (line 351) | type Debug struct
  type Config (line 360) | type Config struct
    method NeedLoadAvgColect (line 778) | func (c *Config) NeedLoadAvgColect() bool {
    method ProcessDataTables (line 826) | func (c *Config) ProcessDataTables() (err error) {
    method setupGraphiteMetrics (line 959) | func (c *Config) setupGraphiteMetrics() bool {
    method GetUserFindLimiter (line 1026) | func (c *Config) GetUserFindLimiter(username string) limiter.ServerLim...
    method GetUserTagsLimiter (line 1036) | func (c *Config) GetUserTagsLimiter(username string) limiter.ServerLim...
  function New (line 374) | func New() *Config {
  function newLoggingConfig (line 466) | func newLoggingConfig() zapwriter.Config {
  function DefaultConfig (line 473) | func DefaultConfig() (*Config, error) {
  function PrintDefaultConfig (line 512) | func PrintDefaultConfig() error {
  function ReadConfig (line 534) | func ReadConfig(filename string, exactConfig bool) (*Config, []zap.Field...
  function Unmarshal (line 549) | func Unmarshal(body []byte, exactConfig bool) (cfg *Config, warns []zap....
  function checkDeprecations (line 903) | func checkDeprecations(cfg *Config, d map[string]error) {
  function CreateCache (line 909) | func CreateCache(cacheName string, cacheConfig *CacheConfig) (cache.Byte...
  function GetQueryParam (line 1047) | func GetQueryParam(a []QueryParam, duration time.Duration) int {

FILE: config/config_test.go
  function TestProcessDataTables (line 23) | func TestProcessDataTables(t *testing.T) {
  function TestKnownDataTableContext (line 228) | func TestKnownDataTableContext(t *testing.T) {
  function TestReadConfig (line 232) | func TestReadConfig(t *testing.T) {
  function TestReadConfigGraphiteWithLimiter (line 444) | func TestReadConfigGraphiteWithLimiter(t *testing.T) {
  function TestReadConfigGraphiteWithALimiter (line 761) | func TestReadConfigGraphiteWithALimiter(t *testing.T) {
  function TestGetQueryParamBroken (line 1088) | func TestGetQueryParamBroken(t *testing.T) {
  function TestGetQueryParam (line 1120) | func TestGetQueryParam(t *testing.T) {
  function TestClickHouse_Validate (line 1297) | func TestClickHouse_Validate(t *testing.T) {

FILE: config/json.go
  method MarshalJSON (line 8) | func (c *ClickHouse) MarshalJSON() ([]byte, error) {

FILE: config/json_test.go
  function TestClickhouseUrlPassword (line 10) | func TestClickhouseUrlPassword(t *testing.T) {

FILE: find/find.go
  type Find (line 17) | type Find struct
    method isResultsLimitExceeded (line 45) | func (f *Find) isResultsLimitExceeded(numResults int) bool {
    method WritePickle (line 50) | func (f *Find) WritePickle(w io.Writer) error {
    method WriteProtobuf (line 94) | func (f *Find) WriteProtobuf(w io.Writer) error {
    method WriteProtobufV3 (line 144) | func (f *Find) WriteProtobufV3(w io.Writer) error {
    method WriteJSON (line 200) | func (f *Find) WriteJSON(w io.Writer) error {
  function NewCached (line 24) | func NewCached(config *config.Config, body []byte) *Find {
  function New (line 31) | func New(config *config.Config, ctx context.Context, query string) (*Fin...

FILE: find/handler.go
  type Handler (line 22) | type Handler struct
    method ServeHTTP (line 34) | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    method Reply (line 228) | func (h *Handler) Reply(w http.ResponseWriter, r *http.Request, f *Fin...
  function NewHandler (line 27) | func NewHandler(config *config.Config) *Handler {

FILE: find/handler_json_test.go
  function NewRequest (line 17) | func NewRequest(method, url string, body io.Reader) *http.Request {
  type testStruct (line 23) | type testStruct struct
  function testResponce (line 30) | func testResponce(t *testing.T, step int, h *Handler, tt *testStruct, wa...
  function TestHandler_ServeValuesJSON (line 52) | func TestHandler_ServeValuesJSON(t *testing.T) {
  function TestHandler_ServeValuesCachedJSON (line 104) | func TestHandler_ServeValuesCachedJSON(t *testing.T) {

FILE: find/handler_test.go
  type clickhouseMock (line 12) | type clickhouseMock struct
    method ServeHTTP (line 16) | func (m *clickhouseMock) ServeHTTP(w http.ResponseWriter, r *http.Requ...
  function TestFind (line 24) | func TestFind(t *testing.T) {

FILE: finder/base.go
  type BaseFinder (line 19) | type BaseFinder struct
    method where (line 36) | func (b *BaseFinder) where(query string) *where.Where {
    method Execute (line 46) | func (b *BaseFinder) Execute(ctx context.Context, config *config.Confi...
    method makeList (line 66) | func (b *BaseFinder) makeList(onlySeries bool) [][]byte {
    method List (line 96) | func (b *BaseFinder) List() [][]byte {
    method Series (line 100) | func (b *BaseFinder) Series() [][]byte {
    method Abs (line 104) | func (b *BaseFinder) Abs(v []byte) []byte {
    method Bytes (line 108) | func (b *BaseFinder) Bytes() ([]byte, error) {
    method Stats (line 112) | func (b *BaseFinder) Stats() []metrics.FinderStat {
  function NewBase (line 27) | func NewBase(url string, table string, opts clickhouse.Options) Finder {

FILE: finder/blacklist.go
  type BlacklistFinder (line 11) | type BlacklistFinder struct
    method Execute (line 24) | func (p *BlacklistFinder) Execute(ctx context.Context, config *config....
    method List (line 35) | func (p *BlacklistFinder) List() [][]byte {
    method Series (line 44) | func (p *BlacklistFinder) Series() [][]byte {
    method Abs (line 52) | func (p *BlacklistFinder) Abs(v []byte) []byte {
    method Bytes (line 56) | func (p *BlacklistFinder) Bytes() ([]byte, error) {
    method Stats (line 60) | func (p *BlacklistFinder) Stats() []metrics.FinderStat {
  function WrapBlacklist (line 17) | func WrapBlacklist(f Finder, blacklist []*regexp.Regexp) *BlacklistFinder {

FILE: finder/date.go
  type DateFinder (line 15) | type DateFinder struct
    method Execute (line 34) | func (b *DateFinder) Execute(ctx context.Context, config *config.Confi...
  function NewDateFinder (line 20) | func NewDateFinder(url string, table string, tableVersion int, opts clic...

FILE: finder/date_reverse.go
  type DateFinderV3 (line 15) | type DateFinderV3 struct
    method whereFilter (line 30) | func (f *DateFinderV3) whereFilter(query string, from int64, until int...
    method Execute (line 43) | func (f *DateFinderV3) Execute(ctx context.Context, config *config.Con...
    method List (line 63) | func (f *DateFinderV3) List() [][]byte {
    method Series (line 72) | func (f *DateFinderV3) Series() [][]byte {
  function NewDateFinderV3 (line 20) | func NewDateFinderV3(url string, table string, opts clickhouse.Options) ...

FILE: finder/date_reverse_test.go
  function TestDateFinderV3_whereFilter (line 11) | func TestDateFinderV3_whereFilter(t *testing.T) {

FILE: finder/finder.go
  type Result (line 13) | type Result interface
  type Finder (line 20) | type Finder interface
  function newPlainFinder (line 25) | func newPlainFinder(ctx context.Context, config *config.Config, query st...
  function Find (line 107) | func Find(config *config.Config, ctx context.Context, query string, from...
  function Leaf (line 116) | func Leaf(value []byte) ([]byte, bool) {
  function FindTagged (line 124) | func FindTagged(ctx context.Context, config *config.Config, terms []Tagg...

FILE: finder/index.go
  constant ReverseLevelOffset (line 19) | ReverseLevelOffset = 10000
  constant TreeLevelOffset (line 20) | TreeLevelOffset = 20000
  constant ReverseTreeLevelOffset (line 21) | ReverseTreeLevelOffset = 30000
  constant DefaultTreeDate (line 23) | DefaultTreeDate = "1970-02-12"
  constant queryAuto (line 26) | queryAuto     = config.IndexAuto
  constant queryDirect (line 27) | queryDirect   = config.IndexDirect
  constant queryReversed (line 28) | queryReversed = config.IndexReversed
  type IndexFinder (line 31) | type IndexFinder struct
    method where (line 69) | func (idx *IndexFinder) where(query string, levelOffset int) *where.Wh...
    method checkReverses (line 80) | func (idx *IndexFinder) checkReverses(query string) uint8 {
    method useReverse (line 100) | func (idx *IndexFinder) useReverse(query string) bool {
    method whereFilter (line 164) | func (idx *IndexFinder) whereFilter(query string, from int64, until in...
    method Execute (line 199) | func (idx *IndexFinder) Execute(ctx context.Context, config *config.Co...
    method Abs (line 228) | func (idx *IndexFinder) Abs(v []byte) []byte {
    method bodySplit (line 263) | func (idx *IndexFinder) bodySplit() {
    method makeList (line 286) | func (idx *IndexFinder) makeList(onlySeries bool) [][]byte {
    method List (line 290) | func (idx *IndexFinder) List() [][]byte {
    method Series (line 294) | func (idx *IndexFinder) Series() [][]byte {
    method Bytes (line 298) | func (idx *IndexFinder) Bytes() ([]byte, error) {
    method Stats (line 302) | func (idx *IndexFinder) Stats() []metrics.FinderStat {
  function NewCachedIndex (line 46) | func NewCachedIndex(body []byte) Finder {
  function NewIndex (line 56) | func NewIndex(url string, table string, dailyEnabled bool, reverse strin...
  function useDaily (line 132) | func useDaily(dailyEnabled bool, from, until int64) bool {
  function calculateIndexLevelOffset (line 136) | func calculateIndexLevelOffset(useDaily, reverse bool) int {
  function addDatesToWhere (line 152) | func addDatesToWhere(w *where.Where, useDaily bool, from, until int64) {
  function validatePlainQuery (line 180) | func validatePlainQuery(query string, wildcardMinDistance int) error {
  function splitIndexBody (line 232) | func splitIndexBody(body []byte, useReverse, useCache bool) ([]byte, [][...
  function makeList (line 272) | func makeList(rows [][]byte, onlySeries bool) [][]byte {

FILE: finder/index_test.go
  function Test_useReverse (line 14) | func Test_useReverse(t *testing.T) {
  function Test_useReverseWithSetConfig (line 35) | func Test_useReverseWithSetConfig(t *testing.T) {
  function Test_checkReverses (line 63) | func Test_checkReverses(t *testing.T) {
  function Benchmark_useReverseDepth (line 96) | func Benchmark_useReverseDepth(b *testing.B) {
  function Benchmark_useReverseDepthPrefixSuffix (line 111) | func Benchmark_useReverseDepthPrefixSuffix(b *testing.B) {
  function Benchmark_useReverseDepthRegex (line 126) | func Benchmark_useReverseDepthRegex(b *testing.B) {
  function TestIndexFinder_whereFilter (line 141) | func TestIndexFinder_whereFilter(t *testing.T) {

FILE: finder/mock.go
  type MockFinder (line 13) | type MockFinder struct
    method Execute (line 33) | func (m *MockFinder) Execute(ctx context.Context, config *config.Confi...
    method List (line 39) | func (m *MockFinder) List() [][]byte {
    method Series (line 44) | func (m *MockFinder) Series() [][]byte {
    method Abs (line 49) | func (m *MockFinder) Abs(v []byte) []byte {
    method Bytes (line 53) | func (m *MockFinder) Bytes() ([]byte, error) {
    method Strings (line 58) | func (m *MockFinder) Strings() []string {
    method Stats (line 63) | func (m *MockFinder) Stats() []metrics.FinderStat {
  function NewMockFinder (line 19) | func NewMockFinder(result [][]byte) *MockFinder {
  function NewMockTagged (line 26) | func NewMockTagged(result [][]byte) *MockFinder {

FILE: finder/plain_from_tagged.go
  type plainFromTaggedFinder (line 16) | type plainFromTaggedFinder struct
    method Target (line 67) | func (f *plainFromTaggedFinder) Target() string {
    method Execute (line 71) | func (f *plainFromTaggedFinder) Execute(ctx context.Context, config *c...
    method Series (line 76) | func (f *plainFromTaggedFinder) Series() [][]byte {
    method Abs (line 85) | func (f *plainFromTaggedFinder) Abs(value []byte) []byte {
    method List (line 126) | func (f *plainFromTaggedFinder) List() [][]byte {
    method Bytes (line 130) | func (f *plainFromTaggedFinder) Bytes() ([]byte, error) {
    method Stats (line 134) | func (f *plainFromTaggedFinder) Stats() []metrics.FinderStat {
  function makePlainFromTagged (line 23) | func makePlainFromTagged(matchers []TaggedTerm) *plainFromTaggedFinder {
  type taggedLabel (line 80) | type taggedLabel struct

FILE: finder/plain_from_tagged_test.go
  function TestPlainFromTaggedFinderAbs (line 9) | func TestPlainFromTaggedFinderAbs(t *testing.T) {

FILE: finder/prefix.go
  type PrefixMatchResult (line 13) | type PrefixMatchResult
  constant PrefixNotMatched (line 16) | PrefixNotMatched PrefixMatchResult = iota
  constant PrefixMatched (line 17) | PrefixMatched
  constant PrefixPartialMathed (line 18) | PrefixPartialMathed
  type PrefixFinder (line 21) | type PrefixFinder struct
    method Execute (line 46) | func (p *PrefixFinder) Execute(ctx context.Context, config *config.Con...
    method List (line 83) | func (p *PrefixFinder) List() [][]byte {
    method Series (line 103) | func (p *PrefixFinder) Series() [][]byte {
    method Abs (line 115) | func (p *PrefixFinder) Abs(value []byte) []byte {
    method Bytes (line 119) | func (p *PrefixFinder) Bytes() ([]byte, error) {
    method Stats (line 123) | func (p *PrefixFinder) Stats() []metrics.FinderStat {
  function bytesConcat (line 29) | func bytesConcat(s1 []byte, s2 []byte) []byte {
  function WrapPrefix (line 37) | func WrapPrefix(f Finder, prefix string) *PrefixFinder {

FILE: finder/prefix_test.go
  function TestPrefixFinderExecute (line 12) | func TestPrefixFinderExecute(t *testing.T) {
  function TestPrefixFinderAbs (line 54) | func TestPrefixFinderAbs(t *testing.T) {
  function TestPrefixFinderList (line 63) | func TestPrefixFinderList(t *testing.T) {

FILE: finder/reverse.go
  type ReverseFinder (line 14) | type ReverseFinder struct
    method Execute (line 54) | func (r *ReverseFinder) Execute(ctx context.Context, config *config.Co...
    method List (line 69) | func (r *ReverseFinder) List() [][]byte {
    method Series (line 82) | func (r *ReverseFinder) Series() [][]byte {
    method Abs (line 95) | func (r *ReverseFinder) Abs(v []byte) []byte {
    method Bytes (line 99) | func (f *ReverseFinder) Bytes() ([]byte, error) {
    method Stats (line 103) | func (f *ReverseFinder) Stats() []metrics.FinderStat {
  function ReverseString (line 22) | func ReverseString(target string) string {
  function ReverseBytes (line 33) | func ReverseBytes(target []byte) []byte {
  function WrapReverse (line 45) | func WrapReverse(f Finder, url string, table string, opts clickhouse.Opt...

FILE: finder/reverse_test.go
  function TestReverse (line 9) | func TestReverse(t *testing.T) {

FILE: finder/split.go
  type indexFinderParams (line 17) | type indexFinderParams struct
  type SplitIndexFinder (line 29) | type SplitIndexFinder struct
    method Execute (line 73) | func (splitFinder *SplitIndexFinder) Execute(
    method whereFilter (line 241) | func (splitFinder *SplitIndexFinder) whereFilter(queries []string, fro...
    method List (line 295) | func (splitFinder *SplitIndexFinder) List() [][]byte {
    method Series (line 304) | func (splitFinder *SplitIndexFinder) Series() [][]byte {
    method Abs (line 314) | func (splitFinder *SplitIndexFinder) Abs(v []byte) []byte {
    method Bytes (line 324) | func (splitFinder *SplitIndexFinder) Bytes() ([]byte, error) {
    method Stats (line 332) | func (splitFinder *SplitIndexFinder) Stats() []metrics.FinderStat {
  function WrapSplitIndex (line 43) | func WrapSplitIndex(
  function splitQuery (line 130) | func splitQuery(query string, maxNodeToSplitIdx int) ([]string, error) {
  function splitPartOfQuery (line 226) | func splitPartOfQuery(prefix, queryPart, suffix string) ([]string, error) {

FILE: finder/split_test.go
  function Test_splitQuery (line 16) | func Test_splitQuery(t *testing.T) {
  function TestSplitIndexFinder_whereFilter (line 203) | func TestSplitIndexFinder_whereFilter(t *testing.T) {

FILE: finder/tag.go
  type TagState (line 16) | type TagState
  constant TagRoot (line 19) | TagRoot     TagState = iota
  constant TagSkip (line 20) | TagSkip
  constant TagInfoRoot (line 21) | TagInfoRoot
  constant TagList (line 22) | TagList
  constant TagListSeriesRoot (line 23) | TagListSeriesRoot
  constant TagListSeries (line 24) | TagListSeries
  constant TagListParam (line 25) | TagListParam
  type TagQ (line 28) | type TagQ struct
    method String (line 33) | func (q TagQ) String() string {
    method Where (line 49) | func (q *TagQ) Where(field string) string {
  type TagFinder (line 65) | type TagFinder struct
    method tagListSQL (line 92) | func (t *TagFinder) tagListSQL() (string, error) {
    method seriesSQL (line 123) | func (t *TagFinder) seriesSQL() (string, error) {
    method MakeSQL (line 149) | func (t *TagFinder) MakeSQL(query string) (string, error) {
    method Execute (line 214) | func (t *TagFinder) Execute(ctx context.Context, config *config.Config...
    method List (line 249) | func (t *TagFinder) List() [][]byte {
    method Series (line 305) | func (t *TagFinder) Series() [][]byte {
    method Abs (line 338) | func (t *TagFinder) Abs(v []byte) []byte {
    method Bytes (line 346) | func (t *TagFinder) Bytes() ([]byte, error) {
    method Stats (line 350) | func (t *TagFinder) Stats() []metrics.FinderStat {
  function WrapTag (line 81) | func WrapTag(f Finder, url string, table string, opts clickhouse.Options...

FILE: finder/tag_test.go
  function TestTagsMakeSQL (line 16) | func TestTagsMakeSQL(t *testing.T) {
  function _TestTags (line 59) | func _TestTags(t *testing.T) {

FILE: finder/tagged.go
  type TaggedTermOp (line 28) | type TaggedTermOp
  constant TaggedTermEq (line 31) | TaggedTermEq       TaggedTermOp = 1
  constant TaggedTermMatch (line 32) | TaggedTermMatch    TaggedTermOp = 2
  constant TaggedTermNe (line 33) | TaggedTermNe       TaggedTermOp = 3
  constant TaggedTermNotMatch (line 34) | TaggedTermNotMatch TaggedTermOp = 4
  type TaggedTerm (line 37) | type TaggedTerm struct
    method concat (line 121) | func (term *TaggedTerm) concat() string {
    method concatMask (line 125) | func (term *TaggedTerm) concatMask() string {
  type TaggedTermList (line 49) | type TaggedTermList
    method Len (line 51) | func (s TaggedTermList) Len() int {
    method Swap (line 54) | func (s TaggedTermList) Swap(i, j int) {
    method Less (line 57) | func (s TaggedTermList) Less(i, j int) bool {
  type TaggedFinder (line 78) | type TaggedFinder struct
    method Execute (line 473) | func (t *TaggedFinder) Execute(ctx context.Context, config *config.Con...
    method whereFilter (line 482) | func (t *TaggedFinder) whereFilter(terms []TaggedTerm, from int64, unt...
    method ExecutePrepared (line 504) | func (t *TaggedFinder) ExecutePrepared(ctx context.Context, terms []Ta...
    method List (line 522) | func (t *TaggedFinder) List() [][]byte {
    method Series (line 547) | func (t *TaggedFinder) Series() [][]byte {
    method Abs (line 598) | func (t *TaggedFinder) Abs(v []byte) []byte {
    method Bytes (line 606) | func (t *TaggedFinder) Bytes() ([]byte, error) {
    method Stats (line 610) | func (t *TaggedFinder) Stats() []metrics.FinderStat {
    method PrepareTaggedTerms (line 614) | func (t *TaggedFinder) PrepareTaggedTerms(ctx context.Context, cfg *co...
  function NewTagged (line 93) | func NewTagged(url string, table, tag1CountTable string, dailyEnabled, u...
  function TaggedTermWhere1 (line 130) | func TaggedTermWhere1(term *TaggedTerm, useCarbonBehaviour, dontMatchMis...
  function TaggedTermWhereN (line 205) | func TaggedTermWhereN(term *TaggedTerm, useCarbonBehaviour, dontMatchMis...
  function setCost (line 284) | func setCost(term *TaggedTerm, costs *config.Costs) {
  function ParseTaggedConditions (line 302) | func ParseTaggedConditions(conditions []string, config *config.Config, a...
  function parseString (line 368) | func parseString(s string) (string, string, error) {
  function seriesByTagArgs (line 389) | func seriesByTagArgs(query string) ([]string, error) {
  function ParseSeriesByTag (line 427) | func ParseSeriesByTag(query string, config *config.Config) ([]TaggedTerm...
  function TaggedWhere (line 440) | func TaggedWhere(terms []TaggedTerm, useCarbonBehaviour, dontMatchMissin...
  function NewCachedTags (line 467) | func NewCachedTags(body []byte) *TaggedFinder {
  function tagsParse (line 551) | func tagsParse(path string) (string, []string, error) {
  function TaggedDecode (line 565) | func TaggedDecode(v []byte) []byte {
  function SortTaggedTermsByCost (line 639) | func SortTaggedTermsByCost(terms []TaggedTerm) {
  function SetCosts (line 674) | func SetCosts(terms []TaggedTerm, costs map[string]*config.Costs) {

FILE: finder/tagged_test.go
  function TestTaggedWhere (line 21) | func TestTaggedWhere(t *testing.T) {
  function TestTaggedWhere_UseCarbonBehaviourFlag (line 122) | func TestTaggedWhere_UseCarbonBehaviourFlag(t *testing.T) {
  function TestTaggedWhere_DontMatchMissingTagsFlag (line 223) | func TestTaggedWhere_DontMatchMissingTagsFlag(t *testing.T) {
  function TestTaggedWhere_BothFeatureFlags (line 326) | func TestTaggedWhere_BothFeatureFlags(t *testing.T) {
  function TestParseSeriesByTag (line 429) | func TestParseSeriesByTag(t *testing.T) {
  function newInt (line 480) | func newInt(i int) *int {
  function TestParseSeriesByTagWithCosts (line 487) | func TestParseSeriesByTagWithCosts(t *testing.T) {
  function BenchmarkParseSeriesByTag (line 594) | func BenchmarkParseSeriesByTag(b *testing.B) {
  function TestParseSeriesByTagWithCostsFromCountTable (line 609) | func TestParseSeriesByTagWithCostsFromCountTable(t *testing.T) {
  function TestTaggedFinder_whereFilter (line 929) | func TestTaggedFinder_whereFilter(t *testing.T) {
  function TestTaggedFinder_Abs (line 1014) | func TestTaggedFinder_Abs(t *testing.T) {

FILE: finder/tags_count_querier.go
  type TagCountQuerier (line 18) | type TagCountQuerier struct
    method GetCostsFromCountTable (line 40) | func (tcq *TagCountQuerier) GetCostsFromCountTable(ctx context.Context...
    method List (line 156) | func (t *TagCountQuerier) List() [][]byte {
    method Stats (line 181) | func (tcq *TagCountQuerier) Stats() []metrics.FinderStat {
  function NewTagCountQuerier (line 29) | func NewTagCountQuerier(url, table string, opts clickhouse.Options, useC...
  function chResultToCosts (line 113) | func chResultToCosts(body [][]byte) (map[string]*config.Costs, error) {
  function parseTag1CountRow (line 134) | func parseTag1CountRow(s string) (string, string, int, error) {

FILE: finder/unescape.go
  function ishex (line 5) | func ishex(c byte) bool {
  function unhex (line 18) | func unhex(c byte) byte {
  function isPercentEscape (line 31) | func isPercentEscape(s string, i int) bool {
  function unescape (line 36) | func unescape(s string) string {

FILE: graphite-clickhouse.go
  constant Version (line 42) | Version = "0.14.0"
  function init (line 44) | func init() {
  type LogResponseWriter (line 48) | type LogResponseWriter struct
    method WriteHeader (line 54) | func (w *LogResponseWriter) WriteHeader(status int) {
    method Status (line 59) | func (w *LogResponseWriter) Status() int {
  function WrapResponseWriter (line 67) | func WrapResponseWriter(w http.ResponseWriter) *LogResponseWriter {
  type App (line 75) | type App struct
    method Handler (line 79) | func (app *App) Handler(handler http.Handler) http.Handler {
  function sdList (line 96) | func sdList(name string, args []string) {
  function sdDelete (line 139) | func sdDelete(name string, args []string) {
  function sdEvict (line 181) | func sdEvict(name string, args []string) {
  function sdExpired (line 220) | func sdExpired(name string, args []string) {
  function sdClean (line 259) | func sdClean(name string, args []string) {
  function printMatchedRollupRules (line 298) | func printMatchedRollupRules(metric string, age uint32, rollupRules *rol...
  function checkRollupMatch (line 326) | func checkRollupMatch(name string, args []string) {
  function main (line 415) | func main() {

FILE: healthcheck/healthcheck.go
  type Handler (line 19) | type Handler struct
    method ServeHTTP (line 35) | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  function NewHandler (line 26) | func NewHandler(config *config.Config) *Handler {

FILE: helper/RowBinary/encode.go
  constant NullUint32 (line 10) | NullUint32 = ^uint32(0)
  function DateToUint16 (line 12) | func DateToUint16(t time.Time) uint16 {
  type Encoder (line 16) | type Encoder struct
    method Date (line 28) | func (w *Encoder) Date(value time.Time) error {
    method Uint8 (line 32) | func (w *Encoder) Uint8(value uint8) error {
    method Uint16 (line 37) | func (w *Encoder) Uint16(value uint16) error {
    method Uint32 (line 44) | func (w *Encoder) Uint32(value uint32) error {
    method NullableUint32 (line 51) | func (w *Encoder) NullableUint32(value uint32) error {
    method Uint64 (line 65) | func (w *Encoder) Uint64(value uint64) error {
    method Float64 (line 72) | func (w *Encoder) Float64(value float64) error {
    method NullableFloat64 (line 76) | func (w *Encoder) NullableFloat64(value float64) error {
    method Bytes (line 90) | func (w *Encoder) Bytes(value []byte) error {
    method String (line 103) | func (w *Encoder) String(value string) error {
    method StringList (line 107) | func (w *Encoder) StringList(value []string) error {
    method Uint32List (line 125) | func (w *Encoder) Uint32List(value []uint32) error {
    method NullableUint32List (line 143) | func (w *Encoder) NullableUint32List(value []uint32) error {
    method Float64List (line 161) | func (w *Encoder) Float64List(value []float64) error {
    method NullableFloat64List (line 179) | func (w *Encoder) NullableFloat64List(value []float64) error {
  function NewEncoder (line 21) | func NewEncoder(w io.Writer) *Encoder {

FILE: helper/clickhouse/clickhouse.go
  type ErrWithDescr (line 30) | type ErrWithDescr struct
    method Error (line 52) | func (e *ErrWithDescr) Error() string {
    method PrependDescription (line 56) | func (e *ErrWithDescr) PrependDescription(test string) {
  type ContentEncoding (line 35) | type ContentEncoding
  constant ContentEncodingNone (line 38) | ContentEncodingNone ContentEncoding = "none"
  constant ContentEncodingGzip (line 39) | ContentEncodingGzip ContentEncoding = "gzip"
  constant ContentEncodingZstd (line 40) | ContentEncodingZstd ContentEncoding = "zstd"
  constant ClickHouseProgressHeader (line 44) | ClickHouseProgressHeader string = "X-Clickhouse-Progress"
  constant ClickHouseSummaryHeader (line 45) | ClickHouseSummaryHeader  string = "X-Clickhouse-Summary"
  function NewErrWithDescr (line 48) | func NewErrWithDescr(err string, data string) error {
  function extractClickhouseError (line 65) | func extractClickhouseError(e string) (int, string) {
  function HandleError (line 94) | func HandleError(w http.ResponseWriter, err error) (status int, queueFai...
  type Options (line 167) | type Options struct
  type LoggedReader (line 175) | type LoggedReader struct
    method Read (line 185) | func (r *LoggedReader) Read(p []byte) (int, error) {
    method Close (line 195) | func (r *LoggedReader) Close() error {
    method ChReadRows (line 206) | func (r *LoggedReader) ChReadRows() int64 {
    method ChReadBytes (line 210) | func (r *LoggedReader) ChReadBytes() int64 {
  type queryStats (line 214) | type queryStats struct
  function formatSQL (line 221) | func formatSQL(q string) string {
  function Query (line 230) | func Query(ctx context.Context, dsn string, query string, opts Options, ...
  function Post (line 234) | func Post(ctx context.Context, dsn string, query string, postBody io.Rea...
  function PostGzip (line 239) | func PostGzip(ctx context.Context, dsn string, query string, postBody io...
  function PostWithEncoding (line 243) | func PostWithEncoding(ctx context.Context, dsn string, query string, pos...
  function Reader (line 247) | func Reader(ctx context.Context, dsn string, query string, opts Options,...
  function reader (line 251) | func reader(ctx context.Context, dsn string, query string, postBody io.R...
  function getQueryStats (line 408) | func getQueryStats(resp *http.Response, statsHeaderName string) (querySt...
  function sendRequestViaDefaultClient (line 470) | func sendRequestViaDefaultClient(request *http.Request, opts *Options) (...
  function sendRequestWithProgressCheck (line 485) | func sendRequestWithProgressCheck(request *http.Request, opts *Options) ...
  function do (line 497) | func do(ctx context.Context, dsn string, query string, postBody io.Reade...
  function ReadUvarint (line 513) | func ReadUvarint(array []byte) (uint64, int, error) {

FILE: helper/clickhouse/clickhouse_test.go
  function Test_extractClickhouseError (line 10) | func Test_extractClickhouseError(t *testing.T) {

FILE: helper/clickhouse/external-data.go
  type ExternalTable (line 18) | type ExternalTable struct
  type Column (line 28) | type Column struct
    method String (line 34) | func (c *Column) String() string {
  type ExternalData (line 40) | type ExternalData struct
    method SetDebug (line 57) | func (e *ExternalData) SetDebug(debugDir string, perm os.FileMode) {
    method buildBody (line 66) | func (e *ExternalData) buildBody(ctx context.Context, u *url.URL) (*by...
    method debugDump (line 111) | func (e *ExternalData) debugDump(ctx context.Context, u url.URL) {
  type extDataDebug (line 45) | type extDataDebug struct
  function NewExternalData (line 51) | func NewExternalData(tables ...ExternalTable) *ExternalData {

FILE: helper/clickhouse/external-data_test.go
  function getTestCases (line 17) | func getTestCases() (tables []ExternalTable) {
  function TestColumnString (line 54) | func TestColumnString(t *testing.T) {
  function TestNewExternalData (line 63) | func TestNewExternalData(t *testing.T) {
  function TestBuildBody (line 71) | func TestBuildBody(t *testing.T) {
  function TestDebugDump (line 102) | func TestDebugDump(t *testing.T) {

FILE: helper/client/datetime.go
  function MetricsTimestampTruncate (line 9) | func MetricsTimestampTruncate(metrics []Metric, precision time.Duration) {

FILE: helper/client/errros.go
  type HttpError (line 5) | type HttpError struct
    method Error (line 17) | func (e *HttpError) Error() string {
  function NewHttpError (line 10) | func NewHttpError(statusCode int, message string) *HttpError {

FILE: helper/client/find.go
  type FindMatch (line 16) | type FindMatch struct
  function MetricsFind (line 23) | func MetricsFind(client *http.Client, address string, format FormatType,...

FILE: helper/client/render.go
  type Metric (line 25) | type Metric struct
  function Render (line 42) | func Render(client *http.Client, address string, format FormatType, targ...
  function Decode (line 151) | func Decode(b []byte, format FormatType) ([]Metric, error) {
  type jsonResponse (line 277) | type jsonResponse struct
  type jsonMetric (line 281) | type jsonMetric struct

FILE: helper/client/requests.go
  type MultiGlobRequestV3 (line 5) | type MultiGlobRequestV3 struct
    method Marshal (line 9) | func (r *MultiGlobRequestV3) Marshal() ([]byte, error) {
    method LogInfo (line 13) | func (r *MultiGlobRequestV3) LogInfo() interface{} {

FILE: helper/client/tags.go
  function TagsNames (line 18) | func TagsNames(client *http.Client, address string, format FormatType, q...
  function TagsValues (line 152) | func TagsValues(client *http.Client, address string, format FormatType, ...

FILE: helper/client/types.go
  type FormatType (line 8) | type FormatType
    method String (line 21) | func (a *FormatType) String() string {
    method Set (line 29) | func (a *FormatType) Set(value string) error {
    method UnmarshalText (line 48) | func (a *FormatType) UnmarshalText(text []byte) error {
  constant FormatDefault (line 11) | FormatDefault FormatType = iota
  constant FormatJSON (line 12) | FormatJSON
  constant FormatProtobuf (line 13) | FormatProtobuf
  constant FormatPb_v2 (line 14) | FormatPb_v2
  constant FormatPb_v3 (line 15) | FormatPb_v3
  constant FormatPickle (line 16) | FormatPickle
  function FormatTypes (line 25) | func FormatTypes() []string {

FILE: helper/date/date.go
  function SetDefault (line 11) | func SetDefault() {
  function SetUTC (line 19) | func SetUTC() {
  function SetBoth (line 27) | func SetBoth() {
  function init (line 34) | func init() {
  function DefaultTimestampToDaysFormat (line 39) | func DefaultTimestampToDaysFormat(ts int64) string {
  function DefaultTimeToDaysFormat (line 45) | func DefaultTimeToDaysFormat(t time.Time) string {
  function UTCTimestampToDaysFormat (line 49) | func UTCTimestampToDaysFormat(timestamp int64) string {
  function UTCTimeToDaysFormat (line 53) | func UTCTimeToDaysFormat(t time.Time) string {
  function defaultDate (line 57) | func defaultDate(t time.Time) time.Time {
  function minLocalAndUTC (line 61) | func minLocalAndUTC(t time.Time) time.Time {
  function MinTimestampToDaysFormat (line 73) | func MinTimestampToDaysFormat(ts int64) string {
  function MinTimeToDaysFormat (line 79) | func MinTimeToDaysFormat(t time.Time) string {
  function maxLocalAndUTC (line 84) | func maxLocalAndUTC(t time.Time) time.Time {
  function MaxTimestampToDaysFormat (line 96) | func MaxTimestampToDaysFormat(ts int64) string {
  function MaxTimeToDaysFormat (line 102) | func MaxTimeToDaysFormat(t time.Time) string {

FILE: helper/date/date_test.go
  function isVerbose (line 12) | func isVerbose() bool {
  function init (line 22) | func init() {
  function TestDefaultTimestampToDaysFormat (line 55) | func TestDefaultTimestampToDaysFormat(t *testing.T) {
  function TestDefaultTimeToDaysFormat (line 101) | func TestDefaultTimeToDaysFormat(t *testing.T) {
  function TestUTCTimestampToDaysFormat (line 139) | func TestUTCTimestampToDaysFormat(t *testing.T) {
  function TestUTCTimeToDaysFormat (line 177) | func TestUTCTimeToDaysFormat(t *testing.T) {
  function TestMinMaxTimestampToDaysFormat (line 215) | func TestMinMaxTimestampToDaysFormat(t *testing.T) {

FILE: helper/datetime/datetime.go
  function parseTime (line 15) | func parseTime(s string) (hour, minute int, err error) {
  function DateParamToEpoch (line 47) | func DateParamToEpoch(s string, tz *time.Location, now time.Time, trunca...
  function Timezone (line 195) | func Timezone(qtz string) (*time.Location, error) {
  function TimestampTruncate (line 203) | func TimestampTruncate(ts int64, truncate time.Duration) int64 {
  function TimeTruncate (line 213) | func TimeTruncate(tm time.Time, truncate time.Duration) time.Time {

FILE: helper/datetime/datetime_test.go
  function TestDateParamToEpoch (line 8) | func TestDateParamToEpoch(t *testing.T) {
  function TestDateParamToEpochTruncate (line 62) | func TestDateParamToEpochTruncate(t *testing.T) {

FILE: helper/errs/errors.go
  type ErrorWithCode (line 5) | type ErrorWithCode struct
    method Error (line 18) | func (e ErrorWithCode) Error() string { return e.err }
  function NewErrorWithCode (line 10) | func NewErrorWithCode(err string, code int) error {
  function NewErrorfWithCode (line 14) | func NewErrorfWithCode(code int, f string, args ...interface{}) error {

FILE: helper/headers/headers.go
  function GetHeaders (line 5) | func GetHeaders(header *http.Header, keys []string) map[string]string {

FILE: helper/http/live-http-client.go
  constant TCPNetwork (line 13) | TCPNetwork string = "tcp"
  function DoHTTPOverTCP (line 15) | func DoHTTPOverTCP(ctx context.Context, transport *http.Transport, req *...

FILE: helper/pickle/pickle.go
  type Writer (line 12) | type Writer struct
    method Mark (line 20) | func (p *Writer) Mark() {
    method Stop (line 24) | func (p *Writer) Stop() {
    method Append (line 28) | func (p *Writer) Append() {
    method SetItem (line 32) | func (p *Writer) SetItem() {
    method List (line 36) | func (p *Writer) List() {
    method Dict (line 40) | func (p *Writer) Dict() {
    method TupleEnd (line 44) | func (p *Writer) TupleEnd() {
    method Bytes (line 48) | func (p *Writer) Bytes(byt []byte) {
    method String (line 63) | func (p *Writer) String(v string) {
    method Uint32 (line 67) | func (p *Writer) Uint32(v uint32) {
    method AppendFloat64 (line 76) | func (p *Writer) AppendFloat64(v float64) {
    method AppendNulls (line 88) | func (p *Writer) AppendNulls(count int) {
    method Bool (line 94) | func (p *Writer) Bool(b bool) {
  function NewWriter (line 16) | func NewWriter(w io.Writer) *Writer {

FILE: helper/point/func.go
  function CleanUp (line 10) | func CleanUp(points []Point) []Point {
  function Uniq (line 29) | func Uniq(points []Point) []Point {
  function FillNulls (line 55) | func FillNulls(points []Point, from, until, step uint32) (start, stop, c...

FILE: helper/point/func_test.go
  function TestUniq (line 14) | func TestUniq(t *testing.T) {
  function TestCleanUp (line 56) | func TestCleanUp(t *testing.T) {
  function TestFillNulls (line 106) | func TestFillNulls(t *testing.T) {

FILE: helper/point/point.go
  type Point (line 5) | type Point struct
  type GetValueOrNaN (line 13) | type GetValueOrNaN

FILE: helper/point/points.go
  type Points (line 9) | type Points struct
    method AppendPoint (line 31) | func (pp *Points) AppendPoint(metricID uint32, value float64, time uin...
    method MetricID (line 41) | func (pp *Points) MetricID(metricName string) uint32 {
    method MetricIDBytes (line 53) | func (pp *Points) MetricIDBytes(metricNameBytes []byte) uint32 {
    method MetricName (line 59) | func (pp *Points) MetricName(metricID uint32) string {
    method List (line 69) | func (pp *Points) List() []Point {
    method ReplaceList (line 74) | func (pp *Points) ReplaceList(list []Point) {
    method GetStep (line 79) | func (pp *Points) GetStep(id uint32) (uint32, error) {
    method SetSteps (line 89) | func (pp *Points) SetSteps(steps map[uint32][]string) {
    method GetAggregation (line 106) | func (pp *Points) GetAggregation(id uint32) (string, error) {
    method SetAggregations (line 116) | func (pp *Points) SetAggregations(functions map[string][]string) {
    method Len (line 133) | func (pp *Points) Len() int {
    method Less (line 137) | func (pp *Points) Less(i, j int) bool {
    method Swap (line 145) | func (pp *Points) Swap(i, j int) {
    method Sort (line 150) | func (pp *Points) Sort() {
    method Uniq (line 155) | func (pp *Points) Uniq() {
    method GroupByMetric (line 161) | func (pp *Points) GroupByMetric() NextMetric {
  type NextMetric (line 19) | type NextMetric
  function NewPoints (line 22) | func NewPoints() *Points {

FILE: helper/rollup/aggr.go
  type Aggr (line 16) | type Aggr struct
    method Name (line 21) | func (ag *Aggr) Name() string {
    method String (line 29) | func (ag *Aggr) String() string {
    method Do (line 37) | func (ag *Aggr) Do(points []point.Point) (r float64) {
  function AggrSum (line 45) | func AggrSum(points []point.Point) (r float64) {
  function AggrMax (line 53) | func AggrMax(points []point.Point) (r float64) {
  function AggrMin (line 67) | func AggrMin(points []point.Point) (r float64) {
  function AggrAvg (line 81) | func AggrAvg(points []point.Point) (r float64) {
  function AggrAny (line 91) | func AggrAny(points []point.Point) (r float64) {
  function AggrAnyLast (line 99) | func AggrAnyLast(points []point.Point) (r float64) {

FILE: helper/rollup/compact.go
  function parseCompact (line 15) | func parseCompact(body string) (*Rules, error) {

FILE: helper/rollup/compact_test.go
  function TestParseCompact (line 9) | func TestParseCompact(t *testing.T) {

FILE: helper/rollup/remote.go
  type rollupRulesResponseRecord (line 17) | type rollupRulesResponseRecord struct
  type rollupRulesResponse (line 25) | type rollupRulesResponse struct
  function parseJson (line 29) | func parseJson(body []byte) (*Rules, error) {
  function RemoteLoad (line 115) | func RemoteLoad(addr string, tlsConf *tls.Config, table string) (*Rules,...

FILE: helper/rollup/remote_test.go
  function assertJsonEqual (line 11) | func assertJsonEqual(t *testing.T, expected string, actual string) {
  function TestParseJson (line 21) | func TestParseJson(t *testing.T) {
  function TestParseJsonTyped (line 129) | func TestParseJsonTyped(t *testing.T) {

FILE: helper/rollup/rollup.go
  type Rollup (line 15) | type Rollup struct
    method Rules (line 79) | func (r *Rollup) Rules() *Rules {
    method update (line 87) | func (r *Rollup) update() error {
    method updateWorker (line 107) | func (r *Rollup) updateWorker() {
    method MarshalJSON (line 122) | func (r *Rollup) MarshalJSON() ([]byte, error) {
  function NewAuto (line 26) | func NewAuto(addr string, tlsConfig *tls.Config, table string, interval ...
  function NewXMLFile (line 41) | func NewXMLFile(filename string, defaultPrecision uint32, defaultFunctio...
  function NewDefault (line 64) | func NewDefault(defaultPrecision uint32, defaultFunction string) (*Rollu...

FILE: helper/rollup/rules.go
  type Retention (line 16) | type Retention struct
  type RuleType (line 21) | type RuleType
    method String (line 34) | func (r *RuleType) String() string {
    method Set (line 38) | func (r *RuleType) Set(value string) error {
    method UnmarshalJSON (line 55) | func (r *RuleType) UnmarshalJSON(data []byte) error {
    method UnmarshalXML (line 64) | func (r *RuleType) UnmarshalXML(d *xml.Decoder, start xml.StartElement...
  constant RuleAll (line 24) | RuleAll RuleType = iota
  constant RulePlain (line 25) | RulePlain
  constant RuleTagged (line 26) | RuleTagged
  constant RuleTagList (line 27) | RuleTagList
  function splitTags (line 73) | func splitTags(tagsStr string) (tags []string) {
  function buildTaggedRegex (line 86) | func buildTaggedRegex(regexpStr string) string {
  type Pattern (line 137) | type Pattern struct
    method compile (line 174) | func (p *Pattern) compile() error {
  type Rules (line 146) | type Rules struct
    method compile (line 211) | func (r *Rules) compile() (*Rules, error) {
    method prepare (line 247) | func (r *Rules) prepare(defaultPrecision uint32, defaultFunction strin...
    method withDefault (line 256) | func (r *Rules) withDefault(defaultPrecision uint32, defaultFunction *...
    method setUpdated (line 275) | func (r *Rules) setUpdated() *Rules {
    method withSuperDefault (line 280) | func (r *Rules) withSuperDefault() *Rules {
    method Lookup (line 285) | func (r *Rules) Lookup(metric string, age uint32, verbose bool) (preci...
    method LookupBytes (line 366) | func (r *Rules) LookupBytes(metric []byte, age uint32, verbose bool) (...
    method RollupMetricAge (line 411) | func (r *Rules) RollupMetricAge(metricName string, age uint32, points ...
    method RollupMetric (line 425) | func (r *Rules) RollupMetric(metricName string, from uint32, points []...
    method RollupPoints (line 438) | func (r *Rules) RollupPoints(pp *point.Points, from int64, step int64)...
  function NewMockRules (line 155) | func NewMockRules(pattern []Pattern, defaultPrecision uint32, defaultFun...
  constant superDefaultPrecision (line 172) | superDefaultPrecision = uint32(60)
  function lookup (line 298) | func lookup(metric string, age uint32, patterns []Pattern, verbose bool)...
  function doMetricPrecision (line 370) | func doMetricPrecision(points []point.Point, precision uint32, aggr *Agg...

FILE: helper/rollup/rules_test.go
  function TestMetricPrecision (line 15) | func TestMetricPrecision(t *testing.T) {
  function Test_buildTaggedRegex (line 36) | func Test_buildTaggedRegex(t *testing.T) {
  function TestLookup (line 95) | func TestLookup(t *testing.T) {
  function TestLookupTyped (line 140) | func TestLookupTyped(t *testing.T) {
  function TestRules_RollupPoints (line 283) | func TestRules_RollupPoints(t *testing.T) {
  function BenchmarkLookupSum (line 705) | func BenchmarkLookupSum(b *testing.B) {
  function BenchmarkLookupSumSeparated (line 718) | func BenchmarkLookupSumSeparated(b *testing.B) {
  function BenchmarkLookupSumTagged (line 731) | func BenchmarkLookupSumTagged(b *testing.B) {
  function BenchmarkLookupSumTaggedSeparated (line 744) | func BenchmarkLookupSumTaggedSeparated(b *testing.B) {
  function BenchmarkLookupMax (line 757) | func BenchmarkLookupMax(b *testing.B) {
  function BenchmarkLookupMaxSeparated (line 770) | func BenchmarkLookupMaxSeparated(b *testing.B) {
  function BenchmarkLookupMaxTagged (line 783) | func BenchmarkLookupMaxTagged(b *testing.B) {
  function BenchmarkLookupMaxTaggedSeparated (line 796) | func BenchmarkLookupMaxTaggedSeparated(b *testing.B) {
  function BenchmarkLookupDefault (line 809) | func BenchmarkLookupDefault(b *testing.B) {
  function BenchmarkLookupDefaultSeparated (line 822) | func BenchmarkLookupDefaultSeparated(b *testing.B) {
  function BenchmarkLookupDefaultTagged (line 835) | func BenchmarkLookupDefaultTagged(b *testing.B) {
  function BenchmarkLookupDefaultTaggedSeparated (line 848) | func BenchmarkLookupDefaultTaggedSeparated(b *testing.B) {

FILE: helper/rollup/xml.go
  type ClickhouseRollupXML (line 39) | type ClickhouseRollupXML struct
  type RetentionXML (line 43) | type RetentionXML struct
    method retention (line 60) | func (r *RetentionXML) retention() Retention {
  type PatternXML (line 48) | type PatternXML struct
    method pattern (line 64) | func (p *PatternXML) pattern() Pattern {
  type RulesXML (line 55) | type RulesXML struct
  function parseXML (line 79) | func parseXML(body []byte) (*Rules, error) {

FILE: helper/rollup/xml_test.go
  function TestParseXML (line 12) | func TestParseXML(t *testing.T) {
  function TestParseXMLTyped (line 115) | func TestParseXMLTyped(t *testing.T) {

FILE: helper/tests/clickhouse/server.go
  type TestResponse (line 11) | type TestResponse struct
  type TestHandler (line 17) | type TestHandler struct
    method ServeHTTP (line 28) | func (h *TestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  type TestServer (line 23) | type TestServer struct
    method AddResponce (line 62) | func (s *TestServer) AddResponce(request string, response *TestRespons...
    method Queries (line 68) | func (s *TestServer) Queries() uint64 {
  function NewTestServer (line 54) | func NewTestServer() *TestServer {

FILE: helper/tests/compare/compare.go
  constant eps (line 5) | eps = 0.0000000001
  function NearlyEqualSlice (line 7) | func NearlyEqualSlice(a, b []float64) bool {
  function NearlyEqual (line 31) | func NearlyEqual(a, b float64) bool {
  function Max (line 48) | func Max(a, b int) int {

FILE: helper/tests/compare/expand/expand.go
  function ExpandTimestamp (line 10) | func ExpandTimestamp(fs *token.FileSet, s string, replace map[string]str...

FILE: helper/utils/utils.go
  function TimestampTruncate (line 6) | func TimestampTruncate(ts int64, duration time.Duration) int64 {

FILE: helper/utils/utils_test.go
  function TestTimestampTruncate (line 9) | func TestTimestampTruncate(t *testing.T) {

FILE: index/handler.go
  type Handler (line 12) | type Handler struct
    method ServeHTTP (line 22) | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  function NewHandler (line 16) | func NewHandler(config *config.Config) *Handler {

FILE: index/index.go
  type Index (line 18) | type Index struct
    method Close (line 67) | func (i *Index) Close() error {
    method WriteJSON (line 71) | func (i *Index) WriteJSON(w http.ResponseWriter) error {
  function New (line 23) | func New(config *config.Config, ctx context.Context) (*Index, error) {

FILE: index/index_test.go
  function TestWriteJSONEmptyRows (line 12) | func TestWriteJSONEmptyRows(t *testing.T) {
  function TestWriteJSONNonleafRows (line 35) | func TestWriteJSONNonleafRows(t *testing.T) {
  function TestWriteJSONEmptyIndex (line 57) | func TestWriteJSONEmptyIndex(t *testing.T) {
  function indexForBytes (line 70) | func indexForBytes(b []byte) *Index {
  function writeRows (line 79) | func writeRows(rows []string) ([]string, error) {

FILE: limiter/alimiter.go
  function getWeighted (line 17) | func getWeighted(n, max int) int {
  type ALimiter (line 40) | type ALimiter struct
    method balance (line 74) | func (sl *ALimiter) balance() int {
    method Capacity (line 104) | func (sl *ALimiter) Capacity() int {
    method Enter (line 108) | func (sl *ALimiter) Enter(ctx context.Context, s string) (err error) {
    method TryEnter (line 134) | func (sl *ALimiter) TryEnter(ctx context.Context, s string) (err error) {
    method Leave (line 160) | func (sl *ALimiter) Leave(ctx context.Context, s string) {
    method SendDuration (line 169) | func (sl *ALimiter) SendDuration(queueMs int64) {
    method Unregiter (line 176) | func (sl *ALimiter) Unregiter() {
    method Enabled (line 181) | func (sl *ALimiter) Enabled() bool {
  function NewALimiter (line 50) | func NewALimiter(capacity, concurrent, n int, enableMetrics bool, scope,...

FILE: limiter/alimiter_test.go
  function Test_getWeighted (line 15) | func Test_getWeighted(t *testing.T) {
  function TestNewALimiter (line 50) | func TestNewALimiter(t *testing.T) {
  type testLimiter (line 125) | type testLimiter struct
  function Benchmark_Limiter_Parallel (line 132) | func Benchmark_Limiter_Parallel(b *testing.B) {

FILE: limiter/interface.go
  type ServerLimiter (line 13) | type ServerLimiter interface

FILE: limiter/limiter.go
  type limiter (line 9) | type limiter struct
    method capacity (line 83) | func (sl *limiter) capacity() int {
    method enter (line 88) | func (sl *limiter) enter(ctx context.Context, s string) error {
    method tryEnter (line 98) | func (sl *limiter) tryEnter(ctx context.Context, s string) error {
    method leave (line 108) | func (sl *limiter) leave(ctx context.Context, s string) {
  type Limiter (line 15) | type Limiter struct
    method Capacity (line 35) | func (sl *Limiter) Capacity() int {
    method Enter (line 40) | func (sl *Limiter) Enter(ctx context.Context, s string) (err error) {
    method TryEnter (line 51) | func (sl *Limiter) TryEnter(ctx context.Context, s string) (err error) {
    method Leave (line 62) | func (sl *Limiter) Leave(ctx context.Context, s string) {
    method SendDuration (line 67) | func (sl *Limiter) SendDuration(queueMs int64) {
    method Unregiter (line 74) | func (sl *Limiter) Unregiter() {
    method Enabled (line 79) | func (sl *Limiter) Enabled() bool {
  function NewLimiter (line 21) | func NewLimiter(capacity int, enableMetrics bool, scope, sub string) Ser...

FILE: limiter/noop.go
  type NoopLimiter (line 8) | type NoopLimiter struct
    method Capacity (line 11) | func (l NoopLimiter) Capacity() int {
    method Enter (line 16) | func (l NoopLimiter) Enter(ctx context.Context, s string) error {
    method TryEnter (line 21) | func (l NoopLimiter) TryEnter(ctx context.Context, s string) error {
    method Leave (line 26) | func (l NoopLimiter) Leave(ctx context.Context, s string) {
    method SendDuration (line 30) | func (l NoopLimiter) SendDuration(queueMs int64) {
    method Unregiter (line 34) | func (l NoopLimiter) Unregiter() {
    method Enabled (line 38) | func (l NoopLimiter) Enabled() bool {

FILE: limiter/wlimiter.go
  type WLimiter (line 10) | type WLimiter struct
    method Capacity (line 42) | func (sl *WLimiter) Capacity() int {
    method Enter (line 46) | func (sl *WLimiter) Enter(ctx context.Context, s string) (err error) {
    method TryEnter (line 72) | func (sl *WLimiter) TryEnter(ctx context.Context, s string) (err error) {
    method Leave (line 98) | func (sl *WLimiter) Leave(ctx context.Context, s string) {
    method SendDuration (line 107) | func (sl *WLimiter) SendDuration(queueMs int64) {
    method Unregiter (line 114) | func (sl *WLimiter) Unregiter() {
    method Enabled (line 119) | func (sl *WLimiter) Enabled() bool {
  function NewWLimiter (line 17) | func NewWLimiter(capacity, concurrent int, enableMetrics bool, scope, su...

FILE: load_avg/load_avg.go
  function Load (line 11) | func Load() float64 {
  function Store (line 15) | func Store(f float64) {
  function Weight (line 19) | func Weight(weight int, degraged, degragedLoadAvg, normalizedLoadAvg flo...

FILE: load_avg/load_avg_default.go
  function Normalized (line 6) | func Normalized() (float64, error) {
  function CpuCount (line 10) | func CpuCount() (uint64, error) {

FILE: load_avg/load_avg_linux.go
  function Normalized (line 14) | func Normalized() (float64, error) {
  function CpuCount (line 33) | func CpuCount() (uint64, error) {

FILE: load_avg/load_avg_test.go
  function TestWeight (line 8) | func TestWeight(t *testing.T) {

FILE: logs/logger.go
  function AccessLog (line 14) | func AccessLog(logger *zap.Logger, config *config.Config, r *http.Reques...

FILE: metrics/metrics.go
  type Config (line 15) | type Config struct
  type CacheMetric (line 33) | type CacheMetric struct
  type ReqMetric (line 44) | type ReqMetric struct
  type WaitMetric (line 60) | type WaitMetric struct
    method Unregister (line 91) | func (w *WaitMetric) Unregister() {
  function NewWaitMetric (line 68) | func NewWaitMetric(enable bool, scope, sub string) WaitMetric {
  function NewDisabledWaitMetric (line 98) | func NewDisabledWaitMetric() *WaitMetric {
  type FindMetrics (line 104) | type FindMetrics struct
  type RenderMetric (line 111) | type RenderMetric struct
  type RenderMetrics (line 116) | type RenderMetrics struct
  function initFindCacheMetrics (line 127) | func initFindCacheMetrics(c *Config) {
  function initFindMetrics (line 152) | func initFindMetrics(scope string, c *Config, waitQueue bool) *FindMetri...
  function initRenderMetrics (line 242) | func initRenderMetrics(scope string, c *Config) *RenderMetrics {
  function SendFindMetrics (line 339) | func SendFindMetrics(r *FindMetrics, statusCode int, durationMs, untilFr...
  function SendRenderMetrics (line 450) | func SendRenderMetrics(r *RenderMetrics, statusCode int, start, fetch, e...
  type rangeName (line 594) | type rangeName struct
  function InitMetrics (line 599) | func InitMetrics(c *Config, findWaitQueue, tagsWaitQueue bool) {
  function DisableMetrics (line 689) | func DisableMetrics() {
  function UnregisterAll (line 695) | func UnregisterAll() {

FILE: metrics/metrics_test.go
  function max (line 14) | func max(a, b int) int {
  function compareInterface (line 22) | func compareInterface(t *testing.T, name string, i interface{}, notNil b...
  function TestInitMetrics (line 31) | func TestInitMetrics(t *testing.T) {

FILE: metrics/query_metrics.go
  type QueryMetric (line 5) | type QueryMetric struct
  type QueryMetrics (line 14) | type QueryMetrics struct
  type FinderStat (line 21) | type FinderStat struct
  function InitQueryMetrics (line 34) | func InitQueryMetrics(table string, c *Config) *QueryMetrics {
  function SendQueryRead (line 83) | func SendQueryRead(r *QueryMetrics, from, until, durationMs, read_rows, ...
  function SendQueryReadChecked (line 116) | func SendQueryReadChecked(r *QueryMetrics, from, until, durationMs, read...
  function SendQueryReadByTable (line 122) | func SendQueryReadByTable(from, until, durationMs, read_rows int64, stat...

FILE: metrics/statsd.go
  type NullSender (line 10) | type NullSender struct
    method Inc (line 12) | func (NullSender) Inc(string, int64, float32, ...statsd.Tag) error    ...
    method Dec (line 13) | func (NullSender) Dec(string, int64, float32, ...statsd.Tag) error    ...
    method Gauge (line 14) | func (NullSender) Gauge(string, int64, float32, ...statsd.Tag) error  ...
    method GaugeDelta (line 15) | func (NullSender) GaugeDelta(string, int64, float32, ...statsd.Tag) er...
    method Timing (line 16) | func (NullSender) Timing(string, int64, float32, ...statsd.Tag) error ...
    method TimingDuration (line 17) | func (NullSender) TimingDuration(string, time.Duration, float32, ...st...
    method Set (line 18) | func (NullSender) Set(string, string, float32, ...statsd.Tag) error   ...
    method SetInt (line 19) | func (NullSender) SetInt(string, int64, float32, ...statsd.Tag) error ...
    method Raw (line 20) | func (NullSender) Raw(string, string, float32, ...statsd.Tag) error   ...
    method NewSubStatter (line 21) | func (NullSender) NewSubStatter(string) statsd.SubStatter             ...
    method SetPrefix (line 22) | func (NullSender) SetPrefix(string)                                   ...
    method SetSamplerFunc (line 23) | func (NullSender) SetSamplerFunc(statsd.SamplerFunc)                  ...
    method Close (line 24) | func (NullSender) Close() error                                       ...

FILE: pkg/alias/map.go
  type Value (line 12) | type Value struct
  type Map (line 18) | type Map struct
    method Merge (line 32) | func (m *Map) Merge(r finder.Result, useCache bool) {
    method MergeTarget (line 37) | func (m *Map) MergeTarget(r finder.Result, target string, saveCache bo...
    method Len (line 73) | func (m *Map) Len() int {
    method Size (line 81) | func (m *Map) Size() int {
    method Series (line 95) | func (m *Map) Series(isReverse bool) []string {
    method DisplayNames (line 110) | func (m *Map) DisplayNames() []string {
    method Get (line 123) | func (m *Map) Get(metric string) []Value {
  function New (line 24) | func New() *Map {

FILE: pkg/alias/map_tagged_test.go
  function createAMTagged (line 20) | func createAMTagged() *Map {
  function TestCreationTagged (line 27) | func TestCreationTagged(t *testing.T) {
  function TestAsyncMergeTagged (line 40) | func TestAsyncMergeTagged(t *testing.T) {
  function Benchmark_MergeTargetTagged (line 99) | func Benchmark_MergeTargetTagged(b *testing.B) {

FILE: pkg/alias/map_test.go
  type Values (line 12) | type Values
    method Len (line 14) | func (v *Values) Len() int {
    method Less (line 18) | func (v *Values) Less(i, j int) bool {
    method Swap (line 22) | func (v *Values) Swap(i, j int) {
  function createAM (line 36) | func createAM() *Map {
  function TestCreation (line 43) | func TestCreation(t *testing.T) {
  function TestAsyncMerge (line 55) | func TestAsyncMerge(t *testing.T) {
  function TestLen (line 113) | func TestLen(t *testing.T) {
  function TestSize (line 125) | func TestSize(t *testing.T) {
  function TestDisplayNames (line 137) | func TestDisplayNames(t *testing.T) {
  function TestGet (line 157) | func TestGet(t *testing.T) {
  function Benchmark_MergeTargetFinder (line 162) | func Benchmark_MergeTargetFinder(b *testing.B) {

FILE: pkg/dry/math.go
  function Max (line 4) | func Max(x, y int64) int64 {
  function Min (line 13) | func Min(x, y int64) int64 {
  function Ceil (line 23) | func Ceil(x, d int64) int64 {
  function CeilToMultiplier (line 33) | func CeilToMultiplier(x, m int64) int64 {
  function FloorToMultiplier (line 39) | func FloorToMultiplier(x, m int64) int64 {
  function GCD (line 48) | func GCD(a, b int64) int64 {
  function LCM (line 64) | func LCM(a, b int64) int64 {

FILE: pkg/dry/math_test.go
  function TestMax (line 9) | func TestMax(t *testing.T) {
  function TestMin (line 17) | func TestMin(t *testing.T) {
  function TestCeil (line 25) | func TestCeil(t *testing.T) {
  function TestCeilToMultiplier (line 34) | func TestCeilToMultiplier(t *testing.T) {
  function TestFloorToMultiplier (line 45) | func TestFloorToMultiplier(t *testing.T) {
  function TestGCD (line 56) | func TestGCD(t *testing.T) {
  function TestLCM (line 67) | func TestLCM(t *testing.T) {

FILE: pkg/dry/strings.go
  function RemoveEmptyStrings (line 4) | func RemoveEmptyStrings(stringList []string) []string {

FILE: pkg/dry/strings_test.go
  function TestRemoveEmptyStrings (line 9) | func TestRemoveEmptyStrings(t *testing.T) {

FILE: pkg/dry/unsafe.go
  function UnsafeString (line 9) | func UnsafeString(b []byte) string {
  function UnsafeStringBytes (line 14) | func UnsafeStringBytes(s *string) []byte {

FILE: pkg/dry/unsafe_test.go
  function TestUnsafeString (line 9) | func TestUnsafeString(t *testing.T) {

FILE: pkg/reverse/reverse.go
  function String (line 8) | func String(path string) string {
  function reverse (line 24) | func reverse(m []byte) {
  function Inplace (line 35) | func Inplace(path []byte) {
  function Bytes (line 55) | func Bytes(path []byte) []byte {

FILE: pkg/reverse/reverse_test.go
  function TestReverse (line 9) | func TestReverse(t *testing.T) {

FILE: pkg/scope/context.go
  type Context (line 10) | type Context struct
    method With (line 20) | func (c *Context) With(key string, value interface{}) *Context {
    method WithRequestID (line 25) | func (c *Context) WithRequestID(requestID string) *Context {
    method WithLogger (line 30) | func (c *Context) WithLogger(logger *zap.Logger) *Context {
    method WithTable (line 35) | func (c *Context) WithTable(table string) *Context {
  function New (line 15) | func New(ctx context.Context) *Context {

FILE: pkg/scope/http_request.go
  function HttpRequest (line 24) | func HttpRequest(r *http.Request) *http.Request {
  function Grafana (line 61) | func Grafana(ctx context.Context) string {

FILE: pkg/scope/key.go
  type scopeKey (line 9) | type scopeKey
  function With (line 12) | func With(ctx context.Context, key string, value interface{}) context.Co...
  function String (line 17) | func String(ctx context.Context, key string) string {
  function Bool (line 26) | func Bool(ctx context.Context, key string) bool {
  function WithRequestID (line 35) | func WithRequestID(ctx context.Context, requestID string) context.Context {
  function RequestID (line 40) | func RequestID(ctx context.Context) string {
  function WithTable (line 45) | func WithTable(ctx context.Context, table string) context.Context {
  function Table (line 50) | func Table(ctx context.Context) string {
  function WithDebug (line 55) | func WithDebug(ctx context.Context, name string) context.Context {
  function Debug (line 60) | func Debug(ctx context.Context, name string) bool {
  function ClickhouseUserAgent (line 65) | func ClickhouseUserAgent(ctx context.Context) string {

FILE: pkg/scope/logger.go
  function Logger (line 18) | func Logger(ctx context.Context) *zap.Logger {
  function LoggerWithHeaders (line 43) | func LoggerWithHeaders(ctx context.Context, r *http.Request, headersToLo...
  function WithLogger (line 78) | func WithLogger(ctx context.Context, logger *zap.Logger) context.Context {

FILE: pkg/where/match.go
  function ClearGlob (line 11) | func ClearGlob(query string) string {
  function HasUnmatchedBrackets (line 102) | func HasUnmatchedBrackets(query string) bool {
  function glob (line 136) | func glob(field string, query string, optionalDotAtEnd bool) string {
  function Glob (line 183) | func Glob(field string, query string) string {
  function TreeGlob (line 188) | func TreeGlob(field string, query string) string {
  function ConcatMatchKV (line 192) | func ConcatMatchKV(key, value string) string {
  function Match (line 205) | func Match(field string, key, value string) string {

FILE: pkg/where/match_test.go
  function Test_ClearGlob (line 5) | func Test_ClearGlob(t *testing.T) {
  function Test_HasUnmatchedBrackets (line 28) | func Test_HasUnmatchedBrackets(t *testing.T) {
  function TestGlob (line 58) | func TestGlob(t *testing.T) {

FILE: pkg/where/where.go
  function unsafeString (line 14) | func unsafeString(b []byte) string {
  function GlobExpandSimple (line 19) | func GlobExpandSimple(value, prefix string, result *[]string) error {
  function GlobToRegexp (line 61) | func GlobToRegexp(g string) string {
  function HasWildcard (line 74) | func HasWildcard(target string) bool {
  function IndexLastWildcard (line 78) | func IndexLastWildcard(target string) int {
  function IndexWildcard (line 82) | func IndexWildcard(target string) int {
  function MaxWildcardDistance (line 86) | func MaxWildcardDistance(query string) int {
  function NonRegexpPrefix (line 99) | func NonRegexpPrefix(expr string) string {
  function escape (line 117) | func escape(s string) string {
  function escapeRegex (line 124) | func escapeRegex(s string) string {
  function likeEscape (line 133) | func likeEscape(s string) string {
  function quote (line 142) | func quote(value interface{}) string {
  function quoteRegex (line 157) | func quoteRegex(key, value string) string {
  function Like (line 166) | func Like(field, s string) string {
  function Eq (line 170) | func Eq(field, value interface{}) string {
  function HasPrefix (line 174) | func HasPrefix(field, prefix string) string {
  function HasPrefixAndNotEq (line 178) | func HasPrefixAndNotEq(field, prefix string) string {
  function HasPrefixBytes (line 182) | func HasPrefixBytes(field, prefix []byte) string {
  function ArrayHas (line 186) | func ArrayHas(field, element string) string {
  function In (line 190) | func In(field string, list []string) string {
  function InTable (line 213) | func InTable(field string, table string) string {
  function DateBetween (line 217) | func DateBetween(field string, from int64, until int64) string {
  function TimestampBetween (line 224) | func TimestampBetween(field string, from int64, until int64) string {
  type Where (line 228) | type Where struct
    method And (line 236) | func (w *Where) And(exp string) {
    method Or (line 248) | func (w *Where) Or(exp string) {
    method Andf (line 260) | func (w *Where) Andf(format string, obj ...interface{}) {
    method String (line 264) | func (w *Where) String() string {
    method SQL (line 268) | func (w *Where) SQL() string {
    method PreWhereSQL (line 276) | func (w *Where) PreWhereSQL() string {
  function New (line 232) | func New() *Where {

FILE: pkg/where/where_test.go
  function TestGlobExpandSimple (line 10) | func TestGlobExpandSimple(t *testing.T) {
  function TestGlobToRegexp (line 41) | func TestGlobToRegexp(t *testing.T) {
  function TestNonRegexpPrefix (line 59) | func TestNonRegexpPrefix(t *testing.T) {
  function TestMaxWildcardDistance (line 78) | func TestMaxWildcardDistance(t *testing.T) {

FILE: prometheus/empty_iterator.go
  type emptyIterator (line 13) | type emptyIterator struct
    method Next (line 19) | func (it *emptyIterator) Next() chunkenc.ValueType { return chunkenc.V...
    method Seek (line 27) | func (it *emptyIterator) Seek(t int64) chunkenc.ValueType { return chu...
    method At (line 31) | func (it *emptyIterator) At() (int64, float64) { return 0, 0 }
    method AtHistogram (line 36) | func (it *emptyIterator) AtHistogram(histogram *histogram.Histogram) (...
    method AtFloatHistogram (line 45) | func (it *emptyIterator) AtFloatHistogram(histogram *histogram.FloatHi...
    method AtT (line 51) | func (it *emptyIterator) AtT() int64 { return 0 }
    method Err (line 55) | func (it *emptyIterator) Err() error { return nil }

FILE: prometheus/exemplar.go
  type nopExemplarQueryable (line 14) | type nopExemplarQueryable struct
    method ExemplarQuerier (line 23) | func (e *nopExemplarQueryable) ExemplarQuerier(ctx context.Context) (s...
  type nopExemplarQuerier (line 17) | type nopExemplarQuerier struct
    method Select (line 27) | func (e *nopExemplarQuerier) Select(start, end int64, matchers ...[]*l...

FILE: prometheus/gatherer.go
  type nopGatherer (line 11) | type nopGatherer struct
    method Gather (line 15) | func (*nopGatherer) Gather() ([]*dto.MetricFamily, error) {

FILE: prometheus/labels.go
  function urlParse (line 14) | func urlParse(rawurl string) (*url.URL, error) {
  function Labels (line 28) | func Labels(path string) labels.Labels {

FILE: prometheus/labels_test.go
  function TestLabels (line 9) | func TestLabels(t *testing.T) {

FILE: prometheus/local_storage.go
  method CleanTombstones (line 17) | func (s *storageImpl) CleanTombstones() error {
  method Delete (line 21) | func (s *storageImpl) Delete(ctx context.Context, mint, maxt int64, ms ....
  method Snapshot (line 25) | func (s *storageImpl) Snapshot(dir string, withHead bool) error {
  method Stats (line 29) | func (s *storageImpl) Stats(statsByLabelName string, limit int) (*tsdb.S...
  method WALReplayStatus (line 35) | func (s *storageImpl) WALReplayStatus() (tsdb.WALReplayStatus, error) {

FILE: prometheus/logger.go
  type errorLevel (line 10) | type errorLevel interface
  type logger (line 14) | type logger struct
    method Log (line 18) | func (l *logger) Log(keyvals ...interface{}) error {

FILE: prometheus/matcher.go
  function makeTaggedFromPromPB (line 29) | func makeTaggedFromPromPB(matchers []*prompb.LabelMatcher) ([]finder.Tag...
  function makeTaggedFromPromQL (line 54) | func makeTaggedFromPromQL(matchers []*labels.Matcher) ([]finder.TaggedTe...

FILE: prometheus/metrics_set.go
  type metricsSet (line 14) | type metricsSet struct
    method At (line 25) | func (ms *metricsSet) At() storage.Series {
    method Err (line 39) | func (ms *metricsSet) Err() error { return nil }
    method Next (line 41) | func (ms *metricsSet) Next() bool {
    method Warnings (line 56) | func (s *metricsSet) Warnings() annotations.Annotations { return nil }
  type metric (line 19) | type metric struct
    method Iterator (line 30) | func (s *metric) Iterator(iterator chunkenc.Iterator) chunkenc.Iterator {
    method Labels (line 34) | func (s *metric) Labels() labels.Labels {
  function newMetricsSet (line 51) | func newMetricsSet(metrics []string) storage.SeriesSet {

FILE: prometheus/querier.go
  type Querier (line 23) | type Querier struct
    method Close (line 30) | func (q *Querier) Close() error {
    method LabelValues (line 35) | func (q *Querier) LabelValues(ctx context.Context, label string, hints...
    method LabelNames (line 74) | func (q *Querier) LabelNames(ctx context.Context, hints *storage.Label...

FILE: prometheus/querier_select.go
  method lookup (line 22) | func (q *Querier) lookup(ctx context.Context, from, until int64, qlimite...
  method timeRange (line 63) | func (q *Querier) timeRange(hints *storage.SelectHints) (int64, int64) {
  method Select (line 95) | func (q *Querier) Select(ctx context.Context, sortSeries bool, hints *st...

FILE: prometheus/querier_select_test.go
  function TestQuerier_timeRange (line 15) | func TestQuerier_timeRange(t *testing.T) {

FILE: prometheus/run.go
  function Run (line 28) | func Run(config *config.Config) error {

FILE: prometheus/run_dummy.go
  function Run (line 10) | func Run(config *config.Config) error {

FILE: prometheus/series_set.go
  type seriesIterator (line 22) | type seriesIterator struct
    method Seek (line 81) | func (sit *seriesIterator) Seek(t int64) chunkenc.ValueType {
    method At (line 99) | func (sit *seriesIterator) At() (t int64, v float64) {
    method AtHistogram (line 117) | func (sit *seriesIterator) AtHistogram(histogram *histogram.Histogram)...
    method AtFloatHistogram (line 127) | func (sit *seriesIterator) AtFloatHistogram(histogram *histogram.Float...
    method AtT (line 134) | func (sit *seriesIterator) AtT() int64 {
    method Next (line 140) | func (sit *seriesIterator) Next() chunkenc.ValueType {
    method Err (line 155) | func (sit *seriesIterator) Err() error { return nil }
  type series (line 30) | type series struct
    method Iterator (line 188) | func (s *series) Iterator(iterator chunkenc.Iterator) chunkenc.Iterator {
    method name (line 192) | func (s *series) name() string {
    method Labels (line 196) | func (s *series) Labels() labels.Labels {
  type seriesSet (line 37) | type seriesSet struct
    method Err (line 158) | func (ss *seriesSet) Err() error { return nil }
    method At (line 160) | func (ss *seriesSet) At() storage.Series {
    method Next (line 171) | func (ss *seriesSet) Next() bool {
    method Warnings (line 183) | func (s *seriesSet) Warnings() annotations.Annotations {
  function makeSeriesSet (line 44) | func makeSeriesSet(data *data.Data, step int64) (storage.SeriesSet, erro...
  function emptySeriesSet (line 71) | func emptySeriesSet() storage.SeriesSet {

FILE: prometheus/storage.go
  type storageImpl (line 13) | type storageImpl struct
    method Querier (line 24) | func (s *storageImpl) Querier(mint, maxt int64) (storage.Querier, erro...
    method ChunkQuerier (line 33) | func (s *storageImpl) ChunkQuerier(mint, maxt int64) (storage.ChunkQue...
    method Appender (line 38) | func (s *storageImpl) Appender(ctx context.Context) storage.Appender {
    method StartTime (line 43) | func (s *storageImpl) StartTime() (int64, error) {
    method Close (line 48) | func (s *storageImpl) Close() error {
  function newStorage (line 19) | func newStorage(config *config.Config) *storageImpl {

FILE: render/data/carbonlink.go
  type carbonlinkFetcher (line 15) | type carbonlinkFetcher interface
  type carbonlinkClient (line 20) | type carbonlinkClient struct
  function setCarbonlinkClient (line 28) | func setCarbonlinkClient(config *config.Carbonlink) {
  function queryCarbonlink (line 52) | func queryCarbonlink(parentCtx context.Context, carbonlink *carbonlinkCl...

FILE: render/data/carbonlink_test.go
  type carbonlinkMocked (line 16) | type carbonlinkMocked struct
    method CacheQueryMulti (line 20) | func (c *carbonlinkMocked) CacheQueryMulti(ctx context.Context, metric...
  function TestSetCarbonlingClient (line 25) | func TestSetCarbonlingClient(t *testing.T) {
  function TestQueryCarbonlink (line 36) | func TestQueryCarbonlink(t *testing.T) {

FILE: render/data/ch_response.go
  type CHResponse (line 14) | type CHResponse struct
    method ToMultiFetchResponseV2 (line 30) | func (c *CHResponse) ToMultiFetchResponseV2() (*v2pb.MultiFetchRespons...
    method ToMultiFetchResponseV3 (line 129) | func (c *CHResponse) ToMultiFetchResponseV3() (*v3pb.MultiFetchRespons...
  type CHResponses (line 24) | type CHResponses
    method ToMultiFetchResponseV2 (line 113) | func (cc *CHResponses) ToMultiFetchResponseV2() (*v2pb.MultiFetchRespo...
    method ToMultiFetchResponseV3 (line 215) | func (cc *CHResponses) ToMultiFetchResponseV3() (*v3pb.MultiFetchRespo...
  function EmptyResponse (line 27) | func EmptyResponse() CHResponses { return CHResponses{{Data: emptyData}} }

FILE: render/data/common_step.go
  type commonStep (line 15) | type commonStep struct
    method addTargets (line 21) | func (c *commonStep) addTargets(delta int) {
    method doneTarget (line 25) | func (c *commonStep) doneTarget() {
    method calculateUnsafe (line 29) | func (c *commonStep) calculateUnsafe(a, b int64) int64 {
    method calculate (line 37) | func (c *commonStep) calculate(value int64) {
    method getResult (line 44) | func (c *commonStep) getResult() int64 {

FILE: render/data/common_step_test.go
  type wrapper (line 10) | type wrapper struct
    method calc (line 16) | func (w *wrapper) calc(step int64) {
  function newWrapper (line 23) | func newWrapper() *wrapper {
  function TestCommonStepWorker (line 36) | func TestCommonStepWorker(t *testing.T) {

FILE: render/data/data.go
  type Data (line 26) | type Data struct
    method GetStep (line 44) | func (d *Data) GetStep(id uint32) (uint32, error) {
    method GetAggregation (line 53) | func (d *Data) GetAggregation(id uint32) (string, error) {
  function contextIsValid (line 34) | func contextIsValid(ctx context.Context) error {
  type data (line 71) | type data struct
    method setSteps (line 136) | func (d *data) setSteps(cond *conditions) {
    method parseResponse (line 239) | func (d *data) parseResponse(ctx context.Context, bodyReader io.ReadCl...
    method wait (line 353) | func (d *data) wait(ctx context.Context) error {
  function prepareData (line 82) | func prepareData(ctx context.Context, targets int, fetcher func() *point...
  function splitErrorHandler (line 146) | func splitErrorHandler(data *[]byte, atEOF bool, tokenLen int, err error...
  function dataSplitAggregated (line 161) | func dataSplitAggregated(data []byte, atEOF bool) (advance int, token []...
  function dataSplitUnaggregated (line 196) | func dataSplitUnaggregated(data []byte, atEOF bool) (advance int, token ...

FILE: render/data/data_parse_test.go
  type pointValues (line 18) | type pointValues struct
  type testPoint (line 24) | type testPoint struct
  function makeAggregatedBody (line 29) | func makeAggregatedBody(points []testPoint) []byte {
  function makeUnaggregatedBody (line 42) | func makeUnaggregatedBody(points []testPoint) []byte {
  function testCarbonlinkReaderNil (line 56) | func testCarbonlinkReaderNil() *point.Points {
  function TestUnaggregatedDataParse (line 60) | func TestUnaggregatedDataParse(t *testing.T) {
  function TestAggregatedDataParse (line 208) | func TestAggregatedDataParse(t *testing.T) {
  function TestPrepareDataParse (line 337) | func TestPrepareDataParse(t *testing.T) {
  function TestAsyncDataParse (line 384) | func TestAsyncDataParse(t *testing.T) {

FILE: render/data/multi_target.go
  type TimeFrame (line 21) | type TimeFrame struct
  type MultiTarget (line 28) | type MultiTarget
    method checkMetricsLimitExceeded (line 56) | func (m *MultiTarget) checkMetricsLimitExceeded(num int) error {
    method Fetch (line 153) | func (m *MultiTarget) Fetch(ctx context.Context, cfg *config.Config, c...
  function MFRToMultiTarget (line 30) | func MFRToMultiTarget(v3Request *v3pb.MultiFetchRequest) MultiTarget {
  function getDataTimeout (line 71) | func getDataTimeout(cfg *config.Config, m *MultiTarget) time.Duration {
  function GetQueryLimiter (line 92) | func GetQueryLimiter(username string, cfg *config.Config, m *MultiTarget...
  function GetQueryLimiterFrom (line 117) | func GetQueryLimiterFrom(username string, cfg *config.Config, from, unti...
  function GetQueryParam (line 133) | func GetQueryParam(username string, cfg *config.Config, m *MultiTarget) ...

FILE: render/data/multi_target_test.go
  function Test_getDataTimeout (line 10) | func Test_getDataTimeout(t *testing.T) {

FILE: render/data/query.go
  constant queryAggregated (line 34) | queryAggregated = `WITH anyResample(%[1]d, %[2]d, %[3]d)(toUInt32(intDiv...
  constant queryUnaggregated (line 45) | queryUnaggregated = `SELECT Path, groupArray(Time), groupArray(Value), g...
  constant extTableName (line 53) | extTableName = "metrics_list"
  type query (line 55) | type query struct
    method appendReply (line 127) | func (q *query) appendReply(chr CHResponse) {
    method getParam (line 133) | func (q *query) getParam(from, until int64) (string, time.Duration) {
    method getDataPoints (line 141) | func (q *query) getDataPoints(ctx context.Context, cond *conditions) e...
    method metricsListExtData (line 284) | func (q *query) metricsListExtData(body *strings.Builder) *clickhouse....
  type conditions (line 69) | type conditions struct
    method prepareMetricsLists (line 301) | func (c *conditions) prepareMetricsLists() {
    method prepareLookup (line 318) | func (c *conditions) prepareLookup() error {
    method setStep (line 382) | func (c *conditions) setStep(cStep *commonStep) {
    method setFromUntil (line 417) | func (c *conditions) setFromUntil() {
    method setPrewhere (line 422) | func (c *conditions) setPrewhere() {
    method setWhere (line 428) | func (c *conditions) setWhere() {
    method generateQuery (line 435) | func (c *conditions) generateQuery(agg string) string {
    method generateQueryaAggregated (line 443) | func (c *conditions) generateQueryaAggregated(agg string) string {
    method generateQueryUnaggregated (line 451) | func (c *conditions) generateQueryUnaggregated() string {
  function newQuery (line 99) | func newQuery(cfg *config.Config, targets int) *query {

FILE: render/data/query_test.go
  function genPattern (line 20) | func genPattern(regexp, function string, retention []rollup.Retention) r...
  function newAM (line 31) | func newAM() *alias.Map {
  function newRules (line 38) | func newRules(reversed bool) *rollup.Rules {
  function ageToTimestamp (line 71) | func ageToTimestamp(age int64) int64 {
  function newCondition (line 76) | func newCondition(fromAge, untilAge, maxDataPoints int64) *conditions {
  function extTableString (line 85) | func extTableString(et map[string]*strings.Builder) map[string]string {
  function TestPrepareMetricsLists (line 94) | func TestPrepareMetricsLists(t *testing.T) {
  function TestPrepareLookup (line 163) | func TestPrepareLookup(t *testing.T) {
  function TestSetStep (line 394) | func TestSetStep(t *testing.T) {
  function TestSetFromUntil (line 470) | func TestSetFromUntil(t *testing.T) {
  function TestGenerateQuery (line 507) | func TestGenerateQuery(t *testing.T) {

FILE: render/data/targets.go
  constant graphiteConsolidationFunction (line 14) | graphiteConsolidationFunction = "consolidateBy"
  type FilteringFunctionsByTarget (line 16) | type FilteringFunctionsByTarget
  type Cache (line 17) | type Cache struct
  type Targets (line 27) | type Targets struct
    method Append (line 66) | func (tt *Targets) Append(target string) {
    method SetFilteringFunctions (line 71) | func (tt *Targets) SetFilteringFunctions(target string, filteringFunct...
    method selectDataTable (line 75) | func (tt *Targets) selectDataTable(cfg *config.Config, tf *TimeFrame, ...
    method GetRequestedAggregation (line 134) | func (tt *Targets) GetRequestedAggregation(target string) (string, err...
  function NewTargets (line 42) | func NewTargets(list []string, am *alias.Map) *Targets {
  function NewTargetsOne (line 53) | func NewTargetsOne(target string, capacity int, am *alias.Map) *Targets {

FILE: render/data/targets_test.go
  function TestSelectDataTableTime (line 12) | func TestSelectDataTableTime(t *testing.T) {
  function TestSelectDataTableMatch (line 104) | func TestSelectDataTableMatch(t *testing.T) {

FILE: render/handler.go
  type Handler (line 28) | type Handler struct
    method finderCached (line 59) | func (h *Handler) finderCached(ts time.Time, fetchRequests data.MultiT...
    method finder (line 145) | func (h *Handler) finder(fetchRequests data.MultiTarget, ctx context.C...
    method ServeHTTP (line 251) | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  function NewHandler (line 33) | func NewHandler(config *config.Config) *Handler {
  function targetKey (line 41) | func targetKey(from, until int64, target, ttl string) string {
  function getCacheTimeout (line 45) | func getCacheTimeout(now time.Time, from, until int64, cacheConfig *conf...

FILE: render/handler_test.go
  function Test_getCacheTimeout (line 11) | func Test_getCacheTimeout(t *testing.T) {

FILE: render/reply/formatter.go
  type Formatter (line 17) | type Formatter interface
  function GetFormatter (line 25) | func GetFormatter(r *http.Request) (Formatter, error) {
  function parseRequestForms (line 53) | func parseRequestForms(r *http.Request) (data.MultiTarget, error) {

FILE: render/reply/formatter_test.go
  function TestFormatterReply (line 53) | func TestFormatterReply(t *testing.T) {
  function prepareCHResponses (line 172) | func prepareCHResponses(from, until int64, indices [][]byte, points map[...
  function emptyValues (line 210) | func emptyValues(size int) []float64 {
  function equalMetrics (line 221) | func equalMetrics(m1, m2 []client.Metric) bool {

FILE: render/reply/json.go
  type JSON (line 20) | type JSON struct
    method ParseRequest (line 110) | func (*JSON) ParseRequest(r *http.Request) (data.MultiTarget, error) {
    method Reply (line 124) | func (*JSON) Reply(w http.ResponseWriter, r *http.Request, multiData d...
  function marshalJSON (line 22) | func marshalJSON(mfr *v3pb.MultiFetchResponse) []byte {
  function parseJSONBody (line 81) | func parseJSONBody(r *http.Request) (data.MultiTarget, error) {

FILE: render/reply/pickle.go
  type Pickle (line 20) | type Pickle struct
    method ParseRequest (line 23) | func (*Pickle) ParseRequest(r *http.Request) (data.MultiTarget, error) {
    method Reply (line 28) | func (*Pickle) Reply(w http.ResponseWriter, r *http.Request, multiData...

FILE: render/reply/protobuf.go
  constant repeated (line 21) | repeated               = 2
  constant flt32 (line 22) | flt32                  = 5
  constant protobufMaxVarintBytes (line 23) | protobufMaxVarintBytes = 10
  type pb (line 26) | type pb interface
  function replyProtobuf (line 31) | func replyProtobuf(p pb, w http.ResponseWriter, r *http.Request, multiDa...
  function init (line 101) | func init() {
  function VarintEncode (line 112) | func VarintEncode(x uint64) []byte {
  function VarintWrite (line 128) | func VarintWrite(w io.Writer, x uint64) {
  function VarintLen (line 139) | func VarintLen(x uint64) uint64 {
  function WriteByteN (line 156) | func WriteByteN(w *bufio.Writer, value byte, n int) {
  function Fixed64Encode (line 163) | func Fixed64Encode(x uint64) []byte {
  function Fixed32Encode (line 176) | func Fixed32Encode(x uint32) []byte {
  function ProtobufWriteSingle (line 185) | func ProtobufWriteSingle(w io.Writer, value float32) {
  function ProtobufWriteDouble (line 189) | func ProtobufWriteDouble(w io.Writer, value float64) {
  function ProtobufWriteDoubleN (line 193) | func ProtobufWriteDoubleN(w io.Writer, value float64, n int) {

FILE: render/reply/protobuf_test.go
  function TestVarintLen (line 8) | func TestVarintLen(t *testing.T) {

FILE: render/reply/v2_pb.go
  type V2PB (line 17) | type V2PB struct
    method ParseRequest (line 23) | func (*V2PB) ParseRequest(r *http.Request) (data.MultiTarget, error) {
    method Reply (line 28) | func (v *V2PB) Reply(w http.ResponseWriter, r *http.Request, multiData...
    method initBuffer (line 36) | func (v *V2PB) initBuffer() {
    method replyDebug (line 41) | func (v *V2PB) replyDebug(w http.ResponseWriter, r *http.Request, mult...
    method writeBody (line 55) | func (v *V2PB) writeBody(writer *bufio.Writer, target, name, function ...

FILE: render/reply/v2_pb_test.go
  type testV2PB (line 14) | type testV2PB struct
  function TestV2PBWriteBody (line 25) | func TestV2PBWriteBody(t *testing.T) {

FILE: render/reply/v3_pb.go
  type V3PB (line 20) | type V3PB struct
    method ParseRequest (line 25) | func (*V3PB) ParseRequest(r *http.Request) (data.MultiTarget, error) {
    method Reply (line 65) | func (v *V3PB) Reply(w http.ResponseWriter, r *http.Request, multiData...
    method initBuffer (line 73) | func (v *V3PB) initBuffer() {
    method replyDebug (line 77) | func (v *V3PB) replyDebug(w http.ResponseWriter, r *http.Request, mult...
    method writeBody (line 91) | func (v *V3PB) writeBody(writer *bufio.Writer, target, name, function ...

FILE: render/reply/v3_pb_test.go
  type testV3PB (line 15) | type testV3PB struct
  function TestV3PBWriteBody (line 26) | func TestV3PBWriteBody(t *testing.T) {

FILE: sd/nginx/nginx.go
  type ErrInvalidKey (line 17) | type ErrInvalidKey struct
    method Error (line 22) | func (e ErrInvalidKey) Error() string {
  function splitNode (line 32) | func splitNode(node string) (dc, host, listen string, ok bool) {
  type Nginx (line 52) | type Nginx struct
    method setWeight (line 93) | func (sd *Nginx) setWeight(weight int64) {
    method Namespace (line 107) | func (sd *Nginx) Namespace() string {
    method List (line 111) | func (sd *Nginx) List() (nodes []string, err error) {
    method ListMap (line 155) | func (sd *Nginx) ListMap() (nodes map[string]string, err error) {
    method Nodes (line 212) | func (sd *Nginx) Nodes() (nodes []utils.KV, err error) {
    method update (line 276) | func (sd *Nginx) update(ip, port string, dc []string) (err error) {
    method Update (line 344) | func (sd *Nginx) Update(ip, port string, dc []string, weight int64) er...
    method DeleteNode (line 350) | func (sd *Nginx) DeleteNode(node string) (err error) {
    method Delete (line 361) | func (sd *Nginx) Delete(ip, port string, dc []string) (err error) {
    method Clear (line 406) | func (sd *Nginx) Clear(preserveIP, preservePort string) (err error) {
  function New (line 65) | func New(url, namespace, hostname string, logger *zap.Logger) *Nginx {

FILE: sd/nginx/nginx_test.go
  function TestNginx (line 30) | func TestNginx(t *testing.T) {
  function TestNginxDC (line 166) | func TestNginxDC(t *testing.T) {

FILE: sd/nginx/tests/nginx_cleanup_test.go
  function cleanup (line 31) | func cleanup(nodes []utils.KV, start, end int64) {
  function TestNginxExpire (line 39) | func TestNginxExpire(t *testing.T) {
  function TestNginxExpireDC (line 99) | func TestNginxExpireDC(t *testing.T) {

FILE: sd/register.go
  type SD (line 24) | type SD interface
  function New (line 41) | func New(cfg *config.Common, hostname string, logger *zap.Logger) (SD, e...
  function Register (line 51) | func Register(cfg *config.Common, logger *zap.Logger) {
  function Stop (line 134) | func Stop() {
  function Cleanup (line 138) | func Cleanup(cfg *config.Common, sd SD, checkOnly bool) error {

FILE: sd/utils/utils.go
  type KV (line 16) | type KV struct
  function HttpGet (line 22) | func HttpGet(url string) ([]byte, error) {
  function HttpPut (line 44) | func HttpPut(url string, body []byte) error {
  function HttpDelete (line 73) | func HttpDelete(url string) error {
  function GetLocalIP (line 101) | func GetLocalIP() string {

FILE: tagger/metric.go
  type Metric (line 10) | type Metric struct
    method ParentPath (line 17) | func (m *Metric) ParentPath() []byte {
    method IsLeaf (line 30) | func (m *Metric) IsLeaf() uint8 {
    method MarshalJSON (line 38) | func (m *Metric) MarshalJSON() ([]byte, error) {

FILE: tagger/rule.go
  type Rule (line 12) | type Rule struct
    method Match (line 204) | func (r *Rule) Match(m *Metric) {
  type Rules (line 28) | type Rules struct
    method Match (line 129) | func (r *Rules) Match(m *Metric) {
    method matchPrefix (line 184) | func (r *Rules) matchPrefix(m *Metric) {
    method matchSuffix (line 188) | func (r *Rules) matchSuffix(m *Metric) {
    method matchContains (line 192) | func (r *Rules) matchContains(m *Metric) {
    method matchOther (line 198) | func (r *Rules) matchOther(m *Metric) {
  function ParseFile (line 36) | func ParseFile(filename string) (*Rules, error) {
  function ParseGlob (line 45) | func ParseGlob(glob string) (*Rules, error) {
  function Parse (line 65) | func Parse(content string) (*Rules, error) {
  function matchByPrefix (line 136) | func matchByPrefix(path []byte, tree *Tree, m *Metric) {
  function matchBySuffix (line 160) | func matchBySuffix(path []byte, tree *Tree, m *Metric) {

FILE: tagger/rule_test.go
  function TestRules (line 33) | func TestRules(t *testing.T) {

FILE: tagger/set.go
  type Set (line 8) | type Set struct
    method Add (line 19) | func (s *Set) Add(tag ...string) *Set {
    method Merge (line 52) | func (s *Set) Merge(other *Set) *Set {
    method Len (line 56) | func (s *Set) Len() int {
    method List (line 60) | func (s *Set) List() []string {
    method MarshalJSON (line 64) | func (s *Set) MarshalJSON() ([]byte, error) {

FILE: tagger/tagger.go
  constant SelectChunksCount (line 27) | SelectChunksCount = 10
  type nopCloser (line 29) | type nopCloser struct
    method Close (line 33) | func (nopCloser) Close() error { return nil }
  function countMetrics (line 35) | func countMetrics(body []byte) (int, error) {
  function pathLevel (line 63) | func pathLevel(path []byte) int {
  function Make (line 75) | func Make(cfg *config.Config) error {
  function cutMetricsIntoParts (line 430) | func cutMetricsIntoParts(metricList []Metric, threads int) ([][]Metric, ...
  function wrapWithCompressor (line 462) | func wrapWithCompressor(cfg *config.Config, writer io.Writer) (io.WriteC...
  function encodeMetricsToRowBinary (line 484) | func encodeMetricsToRowBinary(metricList []Metric, date time.Time, versi...
  function encodeEmptyMetricToRowBinary (line 551) | func encodeEmptyMetricToRowBinary(date time.Time, version uint32, wc io....

FILE: tagger/tagger_test.go
  function TestCutMetricsIntoParts (line 14) | func TestCutMetricsIntoParts(t *testing.T) {
  function TestCutMetricsIntoPartsRandom (line 294) | func TestCutMetricsIntoPartsRandom(t *testing.T) {

FILE: tagger/tree.go
  type Tree (line 3) | type Tree struct
    method Add (line 8) | func (t *Tree) Add(prefix []byte, rule *Rule) {
    method AddSuffix (line 26) | func (t *Tree) AddSuffix(suffix []byte, rule *Rule) {

FILE: tests/clickhouse/rollup/init.sql
  type default (line 1) | CREATE TABLE IF NOT EXISTS default.graphite_reverse (
  type default (line 11) | CREATE TABLE IF NOT EXISTS default.graphite (
  type default (line 21) | CREATE TABLE IF NOT EXISTS default.graphite_index (
  type default (line 30) | CREATE TABLE IF NOT EXISTS default.graphite_tags (
  type default (line 40) | CREATE TABLE IF NOT EXISTS default.tag1_count_per_day

FILE: tests/clickhouse/rollup_tls/init.sql
  type default (line 1) | CREATE TABLE IF NOT EXISTS default.graphite_reverse (
  type default (line 11) | CREATE TABLE IF NOT EXISTS default.graphite (
  type default (line 21) | CREATE TABLE IF NOT EXISTS default.graphite_index (
  type default (line 30) | CREATE TABLE IF NOT EXISTS default.graphite_tags (
  type default (line 40) | CREATE TABLE IF NOT EXISTS default.tag1_count_per_day
Condensed preview — 293 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,542K chars).
[
  {
    "path": ".gitattributes",
    "chars": 0,
    "preview": ""
  },
  {
    "path": ".github/workflows/codeql.yml",
    "chars": 2316,
    "preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
  },
  {
    "path": ".github/workflows/docker.yml",
    "chars": 1371,
    "preview": "name: Docker images\n\non:\n  push:\n    branches: [ master ]\n    tags: [ 'v*' ]\n  pull_request:\n    branches: [ master ]\n\nj"
  },
  {
    "path": ".github/workflows/lint.yml",
    "chars": 339,
    "preview": "name: Lint\non:\n  pull_request:\n\njobs:\n  golangci:\n    name: lint\n    runs-on: ubuntu-22.04\n    steps:\n      - uses: acti"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 2111,
    "preview": "name: Upload Packages to new release\n\non:\n  release:\n    types:\n      - published\n\njobs:\n  build:\n    name: Build\n    ru"
  },
  {
    "path": ".github/workflows/tests-sd.yml",
    "chars": 634,
    "preview": "name: Tests register in SD\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n\n  test"
  },
  {
    "path": ".github/workflows/tests.yml",
    "chars": 2825,
    "preview": "name: Tests\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n\n  tests:\n    env:\n   "
  },
  {
    "path": ".gitignore",
    "chars": 300,
    "preview": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\ngraphite-clickhouse\n.vscode\n/out/\n\n# Fold"
  },
  {
    "path": ".golangci.yml",
    "chars": 1874,
    "preview": "version: \"2\"\nlinters:\n  default: none\n  enable:\n    - asasalint\n    - asciicheck\n    - bidichk\n    - bodyclose\n#    - co"
  },
  {
    "path": "Dockerfile",
    "chars": 436,
    "preview": "FROM golang:alpine as builder\n\nWORKDIR /go/src/github.com/lomik/graphite-clickhouse\nCOPY . .\n\nENV GOPATH=/go\n\nRUN apk ad"
  },
  {
    "path": "LICENSE",
    "chars": 1072,
    "preview": "MIT License\n\nCopyright (c) 2016 Roman Lomonosov\n\nPermission is hereby granted, free of charge, to any person obtaining a"
  },
  {
    "path": "Makefile",
    "chars": 3424,
    "preview": "NAME:=graphite-clickhouse\nDESCRIPTION:=\"Graphite cluster backend with ClickHouse support\"\nMODULE:=github.com/lomik/graph"
  },
  {
    "path": "README.md",
    "chars": 2882,
    "preview": "[![deb](https://img.shields.io/badge/deb-packagecloud.io-844fec.svg)](https://packagecloud.io/go-graphite/stable)\n[![rpm"
  },
  {
    "path": "autocomplete/autocomplete.go",
    "chars": 18883,
    "preview": "package autocomplete\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"g"
  },
  {
    "path": "autocomplete/autocomplete_test.go",
    "chars": 28501,
    "preview": "package autocomplete\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lomik/"
  },
  {
    "path": "cache/cache.go",
    "chars": 2024,
    "preview": "package cache\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/bradfitz/gomemca"
  },
  {
    "path": "capabilities/handler.go",
    "chars": 2666,
    "preview": "package capabilities\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\tv3pb \"github.com/go-graphite/protocol/"
  },
  {
    "path": "cmd/e2e-test/carbon-clickhouse.go",
    "chars": 3286,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"text/template\"\n)\n\nvar CchContainerN"
  },
  {
    "path": "cmd/e2e-test/checks.go",
    "chars": 15551,
    "preview": "package main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"reflect\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/"
  },
  {
    "path": "cmd/e2e-test/clickhouse.go",
    "chars": 5861,
    "preview": "package main\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/"
  },
  {
    "path": "cmd/e2e-test/container.go",
    "chars": 1599,
    "preview": "package main\n\nimport (\n\t\"os/exec\"\n\t\"strings\"\n)\n\nvar (\n\tDockerBinary  string\n\tDockerNetwork string = \"graphite-ch-test\"\n)"
  },
  {
    "path": "cmd/e2e-test/e2etesting.go",
    "chars": 24834,
    "preview": "package main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"go.uber.o"
  },
  {
    "path": "cmd/e2e-test/errors.go",
    "chars": 196,
    "preview": "package main\n\nimport \"errors\"\n\nvar (\n\tErrTimestampInvalid = errors.New(\"invalid timestamp\")\n\tErrNoTest           = error"
  },
  {
    "path": "cmd/e2e-test/graphite-clickhouse.go",
    "chars": 3425,
    "preview": "package main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"syscall\"\n\t\"te"
  },
  {
    "path": "cmd/e2e-test/main.go",
    "chars": 6146,
    "preview": "package main\n\nimport (\n\t\"flag\"\n\t\"log\"\n\t\"os\"\n\t\"path\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"go.uber.org/zap\"\n)\n\ntype MainConfig struct {\n\t"
  },
  {
    "path": "cmd/e2e-test/rproxy.go",
    "chars": 2396,
    "preview": "package main\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\t\"sync\"\n\t\"sync/atomic\""
  },
  {
    "path": "cmd/e2e-test/utils.go",
    "chars": 198,
    "preview": "package main\n\nimport \"os/exec\"\n\nfunc cmdExec(programm string, args ...string) (string, error) {\n\tcmd := exec.Command(pro"
  },
  {
    "path": "cmd/graphite-clickhouse-client/main.go",
    "chars": 5988,
    "preview": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-graphite/protocol"
  },
  {
    "path": "config/.gitignore",
    "chars": 11,
    "preview": "tests_tmp/\n"
  },
  {
    "path": "config/config.go",
    "chars": 46638,
    "preview": "package config\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"os\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t"
  },
  {
    "path": "config/config_test.go",
    "chars": 35233,
    "preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"io/fs\"\n\t\"math\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\"\n\t\"regexp/syntax\"\n\t\"syscall\"\n\t\"testing\"\n\t\"tim"
  },
  {
    "path": "config/json.go",
    "chars": 423,
    "preview": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"net/url\"\n)\n\nfunc (c *ClickHouse) MarshalJSON() ([]byte, error) {\n\ttype Click"
  },
  {
    "path": "config/json_test.go",
    "chars": 536,
    "preview": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestClickhouseUrlPas"
  },
  {
    "path": "deploy/doc/.gitignore",
    "chars": 66,
    "preview": "# autogenerated config for documentation\ngraphite-clickhouse.conf\n"
  },
  {
    "path": "deploy/doc/config.md",
    "chars": 11247,
    "preview": "# Configuration\n\n## Common  `[common]`\n\n### Finder cache\n\nSpecify what storage to use for finder cache. This cache store"
  },
  {
    "path": "deploy/root/usr/lib/systemd/system/graphite-clickhouse.service",
    "chars": 374,
    "preview": "[Unit]\nDescription=Graphite cluster backend with ClickHouse support\nDocumentation=https://github.com/lomik/graphite-clic"
  },
  {
    "path": "doc/aggregation.md",
    "chars": 7052,
    "preview": "# Enable ClickHouse aggregation\n\nThe feature was added in [v0.12.0](https://github.com/lomik/graphite-clickhouse/release"
  },
  {
    "path": "doc/config.md",
    "chars": 22156,
    "preview": "[//]: # (This file is built out of deploy/doc/config.md, please do not edit it manually)  \n[//]: # (To rebuild it run `m"
  },
  {
    "path": "doc/debugging.md",
    "chars": 5605,
    "preview": "# Debug graphite-clickhouse\n## General config\nThe `debug` section contains common parameters:\n\n```toml\n[debug]\ndirectory"
  },
  {
    "path": "doc/graphite_clickhouse.gliffy",
    "chars": 61656,
    "preview": "{\"contentType\":\"application/gliffy+json\",\"version\":\"1.3\",\"stage\":{\"background\":\"#FFFFFF\",\"width\":700,\"height\":710,\"nodeI"
  },
  {
    "path": "doc/index-table.md",
    "chars": 3291,
    "preview": "# Index table\nThe `index` type table is used to look up metrics that [match the query](https://graphite.readthedocs.io/e"
  },
  {
    "path": "doc/release.md",
    "chars": 66,
    "preview": "# New release\n\n- Update `const Version` in graphite-clickhouse.go\n"
  },
  {
    "path": "find/find.go",
    "chars": 4042,
    "preview": "package find\n\nimport (\n\t\"context\"\n\t\"io\"\n\n\t\"github.com/gogo/protobuf/proto\"\n\t\"github.com/msaf1980/go-stringutils\"\n\n\tv2pb "
  },
  {
    "path": "find/handler.go",
    "chars": 6476,
    "preview": "package find\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-graphite/carbonapi/pkg/pa"
  },
  {
    "path": "find/handler_json_test.go",
    "chars": 5208,
    "preview": "package find\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite"
  },
  {
    "path": "find/handler_test.go",
    "chars": 1443,
    "preview": "package find\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\""
  },
  {
    "path": "finder/base.go",
    "chars": 2356,
    "preview": "package finder\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\""
  },
  {
    "path": "finder/blacklist.go",
    "chars": 1201,
    "preview": "package finder\n\nimport (\n\t\"context\"\n\t\"regexp\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n\t\"github.com/lomik/graphit"
  },
  {
    "path": "finder/date.go",
    "chars": 1762,
    "preview": "package finder\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n\t\"github.com/lomik/gr"
  },
  {
    "path": "finder/date_reverse.go",
    "chars": 1878,
    "preview": "package finder\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n\t\"github.com/lomik/graphite-c"
  },
  {
    "path": "finder/date_reverse_test.go",
    "chars": 1723,
    "preview": "package finder\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhouse/helper/clickhouse\"\n\t\"github.com/lomi"
  },
  {
    "path": "finder/finder.go",
    "chars": 4474,
    "preview": "package finder\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/lomik/graphite-clickhouse/helper/clickhouse\"\n\t\"github.com/l"
  },
  {
    "path": "finder/index.go",
    "chars": 7069,
    "preview": "package finder\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/lomik/graphite-clickhouse/confi"
  },
  {
    "path": "finder/index_test.go",
    "chars": 5781,
    "preview": "package finder\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n\t\"github.com/lomik/gr"
  },
  {
    "path": "finder/mock.go",
    "chars": 1477,
    "preview": "package finder\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n\t\"github.com/lom"
  },
  {
    "path": "finder/plain_from_tagged.go",
    "chars": 2746,
    "preview": "package finder\n\nimport (\n\t\"context\"\n\t\"net/url\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/lomik/graphite-clickhouse/con"
  },
  {
    "path": "finder/plain_from_tagged_test.go",
    "chars": 733,
    "preview": "package finder\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPlainFromTaggedFinderAbs(t *testi"
  },
  {
    "path": "finder/prefix.go",
    "chars": 2606,
    "preview": "package finder\n\nimport (\n\t\"context\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n\t\"github.com/lo"
  },
  {
    "path": "finder/prefix_test.go",
    "chars": 2739,
    "preview": "package finder\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n\t\"github.com/stret"
  },
  {
    "path": "finder/reverse.go",
    "chars": 2201,
    "preview": "package finder\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n\t\"github.com/lom"
  },
  {
    "path": "finder/reverse_test.go",
    "chars": 436,
    "preview": "package finder\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestReverse(t *testing.T) {\n\tassert :"
  },
  {
    "path": "finder/split.go",
    "chars": 9012,
    "preview": "package finder\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n\t\"gith"
  },
  {
    "path": "finder/split_test.go",
    "chars": 12383,
    "preview": "package finder\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n\t\"github."
  },
  {
    "path": "finder/tag.go",
    "chars": 7084,
    "preview": "package finder\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n\t\"github."
  },
  {
    "path": "finder/tag_test.go",
    "chars": 3668,
    "preview": "package finder\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/lomik"
  },
  {
    "path": "finder/tagged.go",
    "chars": 18042,
    "preview": "package finder\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/lomik/graphite-clickhou"
  },
  {
    "path": "finder/tagged_test.go",
    "chars": 52535,
    "preview": "package finder\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"sort\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite"
  },
  {
    "path": "finder/tags_count_querier.go",
    "chars": 4342,
    "preview": "package finder\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n\t\"github."
  },
  {
    "path": "finder/unescape.go",
    "chars": 1053,
    "preview": "package finder\n\nimport \"strings\"\n\nfunc ishex(c byte) bool {\n\tswitch {\n\tcase '0' <= c && c <= '9':\n\t\treturn true\n\tcase 'a"
  },
  {
    "path": "go.mod",
    "chars": 7078,
    "preview": "module github.com/lomik/graphite-clickhouse\n\ngo 1.23.1\n\nrequire (\n\tgithub.com/BurntSushi/toml v0.3.1\n\tgithub.com/bradfit"
  },
  {
    "path": "go.sum",
    "chars": 87284,
    "preview": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1"
  },
  {
    "path": "graphite-clickhouse.go",
    "chars": 17517,
    "preview": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"math/rand\"\n\t\"net/http\"\n\t_ \"net/http/ppr"
  },
  {
    "path": "healthcheck/healthcheck.go",
    "chars": 2829,
    "preview": "package healthcheck\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/lomik/"
  },
  {
    "path": "helper/RowBinary/encode.go",
    "chars": 3464,
    "preview": "package RowBinary\n\nimport (\n\t\"encoding/binary\"\n\t\"io\"\n\t\"math\"\n\t\"time\"\n)\n\nconst NullUint32 = ^uint32(0)\n\nfunc DateToUint16"
  },
  {
    "path": "helper/clickhouse/clickhouse.go",
    "chars": 14122,
    "preview": "package clickhouse\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"html\"\n\t\"io\""
  },
  {
    "path": "helper/clickhouse/clickhouse_test.go",
    "chars": 5183,
    "preview": "package clickhouse\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_extractClickhous"
  },
  {
    "path": "helper/clickhouse/external-data.go",
    "chars": 3403,
    "preview": "package clickhouse\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"mime/multipart\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github."
  },
  {
    "path": "helper/clickhouse/external-data_test.go",
    "chars": 3169,
    "preview": "package clickhouse\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"g"
  },
  {
    "path": "helper/client/datetime.go",
    "chars": 597,
    "preview": "package client\n\nimport (\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhouse/helper/datetime\"\n)\n\nfunc MetricsTimestampTrunca"
  },
  {
    "path": "helper/client/errros.go",
    "chars": 327,
    "preview": "package client\n\nimport \"strconv\"\n\ntype HttpError struct {\n\tstatusCode int\n\tmessage    string\n}\n\nfunc NewHttpError(status"
  },
  {
    "path": "helper/client/find.go",
    "chars": 3222,
    "preview": "package client\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\n\tprotov2 \"github.com/go-graphite/protocol/carbona"
  },
  {
    "path": "helper/client/render.go",
    "chars": 7111,
    "preview": "package client\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"st"
  },
  {
    "path": "helper/client/requests.go",
    "chars": 323,
    "preview": "package client\n\nimport protov3 \"github.com/go-graphite/protocol/carbonapi_v3_pb\"\n\ntype MultiGlobRequestV3 struct {\n\tprot"
  },
  {
    "path": "helper/client/tags.go",
    "chars": 6674,
    "preview": "package client\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github."
  },
  {
    "path": "helper/client/types.go",
    "chars": 1040,
    "preview": "package client\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n)\n\ntype FormatType int\n\nconst (\n\tFormatDefault FormatType = iota\n\tFormatJSON\n\t"
  },
  {
    "path": "helper/date/date.go",
    "chars": 3086,
    "preview": "package date\n\nimport \"time\"\n\nvar FromTimestampToDaysFormat func(int64) string\nvar FromTimeToDaysFormat func(time.Time) s"
  },
  {
    "path": "helper/date/date_test.go",
    "chars": 8643,
    "preview": "package date\n\nimport (\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n)\n\nvar verbose bool\n\nfunc isVerbose() bool {\n\tfor _, arg := r"
  },
  {
    "path": "helper/datetime/datetime.go",
    "chars": 4510,
    "preview": "package datetime\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-graphite/carbonapi/pkg/parser\"\n)\n\nva"
  },
  {
    "path": "helper/datetime/datetime_test.go",
    "chars": 2968,
    "preview": "package datetime\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestDateParamToEpoch(t *testing.T) {\n\ttimeZone := time.Local\n\t//16"
  },
  {
    "path": "helper/errs/errors.go",
    "chars": 367,
    "preview": "package errs\n\nimport \"fmt\"\n\ntype ErrorWithCode struct {\n\terr  string\n\tCode int // error code\n}\n\nfunc NewErrorWithCode(er"
  },
  {
    "path": "helper/headers/headers.go",
    "chars": 316,
    "preview": "package headers\n\nimport \"net/http\"\n\nfunc GetHeaders(header *http.Header, keys []string) map[string]string {\n\tif len(keys"
  },
  {
    "path": "helper/http/live-http-client.go",
    "chars": 1196,
    "preview": "package http\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n)\n\nconst TCPNetwork string = \"tcp\"\n"
  },
  {
    "path": "helper/pickle/pickle.go",
    "chars": 1474,
    "preview": "package pickle\n\nimport (\n\t\"encoding/binary\"\n\t\"io\"\n\t\"math\"\n)\n\nvar EmptyList = []byte{0x28, 0x6c, 0x70, 0x30, 0xa, 0x2e}\n\n"
  },
  {
    "path": "helper/point/func.go",
    "chars": 2496,
    "preview": "package point\n\nimport (\n\t\"fmt\"\n\t\"math\"\n)\n\n// CleanUp removes points with empty metric\n// for run after Deduplicate, Merg"
  },
  {
    "path": "helper/point/func_test.go",
    "chars": 5156,
    "preview": "package point\n\nimport (\n\t\"errors\"\n\t\"math\"\n\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar nan = math.NaN()\n\nfu"
  },
  {
    "path": "helper/point/point.go",
    "chars": 780,
    "preview": "package point\n\nimport \"fmt\"\n\ntype Point struct {\n\tMetricID  uint32\n\tValue     float64\n\tTime      uint32\n\tTimestamp uint3"
  },
  {
    "path": "helper/point/points.go",
    "chars": 4488,
    "preview": "package point\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n)\n\n// Points is a structure that stores points and additional information about t"
  },
  {
    "path": "helper/rollup/aggr.go",
    "chars": 1512,
    "preview": "package rollup\n\nimport (\n\t\"github.com/lomik/graphite-clickhouse/helper/point\"\n)\n\nvar AggrMap = map[string]*Aggr{\n\t\"avg\":"
  },
  {
    "path": "helper/rollup/compact.go",
    "chars": 1440,
    "preview": "package rollup\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n/*\ncompact form of rollup rules for tests\n\nregexp;function;age:"
  },
  {
    "path": "helper/rollup/compact_test.go",
    "chars": 667,
    "preview": "package rollup\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParseCompact(t *testing.T) {\n\tcon"
  },
  {
    "path": "helper/rollup/remote.go",
    "chars": 4335,
    "preview": "package rollup\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/lomik/zap"
  },
  {
    "path": "helper/rollup/remote_test.go",
    "chars": 6724,
    "preview": "package rollup\n\nimport (\n\t\"encoding/json\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc assertJson"
  },
  {
    "path": "helper/rollup/rollup.go",
    "chars": 2602,
    "preview": "package rollup\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/lomik/zapwriter\"\n\t\"go"
  },
  {
    "path": "helper/rollup/rules.go",
    "chars": 11148,
    "preview": "package rollup\n\nimport (\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickh"
  },
  {
    "path": "helper/rollup/rules_test.go",
    "chars": 19897,
    "preview": "package rollup\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhouse/helper/p"
  },
  {
    "path": "helper/rollup/xml.go",
    "chars": 2132,
    "preview": "package rollup\n\nimport (\n\t\"encoding/xml\"\n)\n\n/*\n<graphite_rollup>\n \t<pattern>\n \t\t<regexp>click_cost</regexp>\n \t\t<function"
  },
  {
    "path": "helper/rollup/xml_test.go",
    "chars": 7939,
    "preview": "package rollup\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testif"
  },
  {
    "path": "helper/tests/clickhouse/server.go",
    "chars": 1293,
    "preview": "package clickhouse\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"sync\"\n\t\"sync/atomic\"\n)\n\ntype TestResponse struct {"
  },
  {
    "path": "helper/tests/compare/compare.go",
    "chars": 711,
    "preview": "package compare\n\nimport \"math\"\n\nconst eps = 0.0000000001\n\nfunc NearlyEqualSlice(a, b []float64) bool {\n\tif len(a) != len"
  },
  {
    "path": "helper/tests/compare/expand/expand.go",
    "chars": 418,
    "preview": "package expand\n\nimport (\n\t\"go/token\"\n\t\"go/types\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc ExpandTimestamp(fs *token.FileSet, s stri"
  },
  {
    "path": "helper/utils/utils.go",
    "chars": 224,
    "preview": "package utils\n\nimport \"time\"\n\n// TimestampTruncate truncate timestamp with duration\nfunc TimestampTruncate(ts int64, dur"
  },
  {
    "path": "helper/utils/utils_test.go",
    "chars": 891,
    "preview": "package utils\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestTimestampTruncate(t *testing.T) {\n\t// reverse sorted\n\ttest"
  },
  {
    "path": "index/handler.go",
    "chars": 1005,
    "preview": "package index\n\nimport (\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n\t\"github.com/lomik/graphite-"
  },
  {
    "path": "index/index.go",
    "chars": 2364,
    "preview": "package index\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/lomik/graph"
  },
  {
    "path": "index/index_test.go",
    "chars": 1946,
    "preview": "package index\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestWriteJSON"
  },
  {
    "path": "issues/daytime/carbon-clickhouse.conf.tpl",
    "chars": 806,
    "preview": "[common]\n\n[data]\npath = \"/etc/carbon-clickhouse/data\"\nchunk-interval = \"1s\"\nchunk-auto-interval = \"\"\n\n[upload.graphite_i"
  },
  {
    "path": "issues/daytime/graphite-clickhouse-internal-aggr.conf.tpl",
    "chars": 802,
    "preview": "[common]\nlisten = \"{{ .GCH_ADDR }}\"\nmax-cpu = 0\nmax-metrics-in-render-answer = 10000\nmax-metrics-per-target = 10000\nhead"
  },
  {
    "path": "issues/daytime/graphite-clickhouse.conf.tpl",
    "chars": 803,
    "preview": "[common]\nlisten = \"{{ .GCH_ADDR }}\"\nmax-cpu = 0\nmax-metrics-in-render-answer = 10000\nmax-metrics-per-target = 10000\nhead"
  },
  {
    "path": "issues/daytime/test.toml",
    "chars": 11677,
    "preview": "[test]\nprecision = \"10s\"\n\n[[test.clickhouse]]\nversion = \"24.2\"\ndir = \"tests/clickhouse/rollup\"\n\n[test.carbon_clickhouse]"
  },
  {
    "path": "limiter/alimiter.go",
    "chars": 3500,
    "preview": "package limiter\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhouse/load_avg\"\n\t\"github.com/lomik/graphi"
  },
  {
    "path": "limiter/alimiter_test.go",
    "chars": 4561,
    "preview": "package limiter\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhous"
  },
  {
    "path": "limiter/interface.go",
    "chars": 403,
    "preview": "package limiter\n\nimport (\n\t\"context\"\n\t\"errors\"\n)\n\nvar (\n\tErrTimeout  = errors.New(\"timeout exceeded\")\n\tErrOverflow = err"
  },
  {
    "path": "limiter/limiter.go",
    "chars": 2352,
    "preview": "package limiter\n\nimport (\n\t\"context\"\n\n\t\"github.com/lomik/graphite-clickhouse/metrics\"\n)\n\ntype limiter struct {\n\tch  chan"
  },
  {
    "path": "limiter/noop.go",
    "chars": 874,
    "preview": "package limiter\n\nimport (\n\t\"context\"\n)\n\n// ServerLimiter provides interface to limit amount of requests\ntype NoopLimiter"
  },
  {
    "path": "limiter/wlimiter.go",
    "chars": 2557,
    "preview": "package limiter\n\nimport (\n\t\"context\"\n\n\t\"github.com/lomik/graphite-clickhouse/metrics\"\n)\n\n// WLimiter provide limiter amo"
  },
  {
    "path": "load_avg/load_avg.go",
    "chars": 734,
    "preview": "package load_avg\n\nimport (\n\t\"math\"\n\n\t\"github.com/msaf1980/go-syncutils/atomic\"\n)\n\nvar loadAvgStore atomic.Float64\n\nfunc "
  },
  {
    "path": "load_avg/load_avg_default.go",
    "chars": 160,
    "preview": "//go:build !linux\n// +build !linux\n\npackage load_avg\n\nfunc Normalized() (float64, error) {\n\treturn 0, nil\n}\n\nfunc CpuCou"
  },
  {
    "path": "load_avg/load_avg_linux.go",
    "chars": 669,
    "preview": "//go:build linux\n// +build linux\n\npackage load_avg\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/msaf1980/go-strin"
  },
  {
    "path": "load_avg/load_avg_test.go",
    "chars": 3828,
    "preview": "package load_avg\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestWeight(t *testing.T) {\n\ttests := []struct {\n\t\tweight          i"
  },
  {
    "path": "logs/logger.go",
    "chars": 1067,
    "preview": "package logs\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n\t\"github.co"
  },
  {
    "path": "metrics/limiter_metrics.go",
    "chars": 16,
    "preview": "package metrics\n"
  },
  {
    "path": "metrics/metrics.go",
    "chars": 23159,
    "preview": "package metrics\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/msaf1980/go-metrics\"\n\t\"github.com/msaf1980/go-met"
  },
  {
    "path": "metrics/metrics_test.go",
    "chars": 19800,
    "preview": "package metrics\n\nimport (\n\t\"math\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/msaf1980/go-metrics\"\n\t\"github.com/msaf1980"
  },
  {
    "path": "metrics/query_metrics.go",
    "chars": 4145,
    "preview": "package metrics\n\nimport \"github.com/msaf1980/go-metrics\"\n\ntype QueryMetric struct {\n\tRequestsH       metrics.Histogram\n\t"
  },
  {
    "path": "metrics/statsd.go",
    "chars": 1517,
    "preview": "package metrics\n\nimport (\n\t\"time\"\n\n\t\"github.com/cactus/go-statsd-client/v5/statsd\"\n)\n\n// NullSender  is disabled sender "
  },
  {
    "path": "nfpm.yaml",
    "chars": 801,
    "preview": "---\nname: ${NAME}\ndescription: ${DESCRIPTION}\n\n# Common packages config\narch: \"${ARCH}\"  # amd64, arm64\nplatform: \"linux"
  },
  {
    "path": "packages.sh",
    "chars": 566,
    "preview": "#!/bin/sh -e\n\ncd \"$( dirname \"$0\" )\"\nROOT=$PWD\n\ndocker run -i -e \"DEVEL=${DEVEL:-0}\"  --rm -v \"$ROOT:/root/go/src/github"
  },
  {
    "path": "pkg/alias/map.go",
    "chars": 2205,
    "preview": "package alias\n\nimport (\n\t\"bytes\"\n\t\"sync\"\n\n\t\"github.com/lomik/graphite-clickhouse/finder\"\n\t\"github.com/lomik/graphite-cli"
  },
  {
    "path": "pkg/alias/map_tagged_test.go",
    "chars": 2849,
    "preview": "package alias\n\nimport (\n\t\"sort\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/lomik/graphite-clickhouse/finder\"\n\t\"github.com/stretchr"
  },
  {
    "path": "pkg/alias/map_test.go",
    "chars": 3904,
    "preview": "package alias\n\nimport (\n\t\"sort\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/lomik/graphite-clickhouse/finder\"\n\t\"github.com/stretchr"
  },
  {
    "path": "pkg/dry/math.go",
    "chars": 1346,
    "preview": "package dry\n\n// Max returns the larger of x or y.\nfunc Max(x, y int64) int64 {\n\tif x > y {\n\t\treturn x\n\t}\n\n\treturn y\n}\n\n/"
  },
  {
    "path": "pkg/dry/math_test.go",
    "chars": 2027,
    "preview": "package dry\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestMax(t *testing.T) {\n\tassert := asser"
  },
  {
    "path": "pkg/dry/strings.go",
    "chars": 353,
    "preview": "package dry\n\n// RemoveEmptyStrings removes empty strings from list and returns truncated slice\nfunc RemoveEmptyStrings(s"
  },
  {
    "path": "pkg/dry/strings_test.go",
    "chars": 268,
    "preview": "package dry\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRemoveEmptyStrings(t *testing.T) {\n\t"
  },
  {
    "path": "pkg/dry/unsafe.go",
    "chars": 364,
    "preview": "package dry\n\nimport (\n\t\"reflect\"\n\t\"unsafe\"\n)\n\n// UnsafeString returns string object from byte slice without copying\nfunc"
  },
  {
    "path": "pkg/dry/unsafe_test.go",
    "chars": 335,
    "preview": "package dry\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestUnsafeString(t *testing.T) {\n\tassert"
  },
  {
    "path": "pkg/reverse/reverse.go",
    "chars": 866,
    "preview": "package reverse\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n)\n\nfunc String(path string) string {\n\t// don't reverse tagged path\n\tif str"
  },
  {
    "path": "pkg/reverse/reverse_test.go",
    "chars": 800,
    "preview": "package reverse\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestReverse(t *testing.T) {\n\tassert "
  },
  {
    "path": "pkg/scope/context.go",
    "chars": 731,
    "preview": "package scope\n\nimport (\n\t\"context\"\n\n\t\"go.uber.org/zap\"\n)\n\n// Context wrapper for context.Context with chain constructor\n"
  },
  {
    "path": "pkg/scope/http_request.go",
    "chars": 1587,
    "preview": "package scope\n\nimport (\n\t\"context\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nvar"
  },
  {
    "path": "pkg/scope/key.go",
    "chars": 1797,
    "preview": "package scope\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\n// key is type for context.Value keys\ntype scopeKey string\n\n// With returns"
  },
  {
    "path": "pkg/scope/logger.go",
    "chars": 1686,
    "preview": "package scope\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/lomik/graphite-clickhouse/helper/headers\"\n\t\"github.com/lomi"
  },
  {
    "path": "pkg/scope/version.go",
    "chars": 34,
    "preview": "package scope\n\nvar Version string\n"
  },
  {
    "path": "pkg/where/match.go",
    "chars": 4146,
    "preview": "package where\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\nvar opEq = \"=\"\n\n// ClearGlob cleanup grafana globs like {name}\nfunc ClearGl"
  },
  {
    "path": "pkg/where/match_test.go",
    "chars": 2314,
    "preview": "package where\n\nimport \"testing\"\n\nfunc Test_ClearGlob(t *testing.T) {\n\ttests := []struct {\n\t\tquery string\n\t\twant  string\n"
  },
  {
    "path": "pkg/where/where.go",
    "chars": 5726,
    "preview": "package where\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"unsafe\"\n\n\t\"github.com/lomik/graphite-clickhouse/helper"
  },
  {
    "path": "pkg/where/where_test.go",
    "chars": 2406,
    "preview": "package where\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGlobExpandSimple(t *testing"
  },
  {
    "path": "prometheus/.gitignore",
    "chars": 4,
    "preview": "tmp\n"
  },
  {
    "path": "prometheus/empty_iterator.go",
    "chars": 2383,
    "preview": "//go:build !noprom\n// +build !noprom\n\npackage prometheus\n\nimport (\n\t\"github.com/prometheus/prometheus/model/histogram\"\n\t"
  },
  {
    "path": "prometheus/exemplar.go",
    "chars": 713,
    "preview": "//go:build !noprom\n// +build !noprom\n\npackage prometheus\n\nimport (\n\t\"context\"\n\n\t\"github.com/prometheus/prometheus/model/"
  },
  {
    "path": "prometheus/gatherer.go",
    "chars": 333,
    "preview": "//go:build !noprom\n// +build !noprom\n\npackage prometheus\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\tdt"
  },
  {
    "path": "prometheus/labels.go",
    "chars": 815,
    "preview": "//go:build !noprom\n// +build !noprom\n\npackage prometheus\n\nimport (\n\t\"net/url\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/prometheu"
  },
  {
    "path": "prometheus/labels_test.go",
    "chars": 652,
    "preview": "package prometheus\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestLabels(t *testing.T) {\n\tasser"
  },
  {
    "path": "prometheus/local_storage.go",
    "chars": 832,
    "preview": "//go:build !noprom\n// +build !noprom\n\npackage prometheus\n\nimport (\n\t\"context\"\n\n\t\"github.com/prometheus/prometheus/model/"
  },
  {
    "path": "prometheus/logger.go",
    "chars": 1162,
    "preview": "//go:build !noprom\n// +build !noprom\n\npackage prometheus\n\nimport (\n\t\"go.uber.org/zap\"\n)\n\ntype errorLevel interface {\n\tSt"
  },
  {
    "path": "prometheus/matcher.go",
    "chars": 3436,
    "preview": "//go:build !noprom\n// +build !noprom\n\npackage prometheus\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/lomik/graphite-clickhous"
  },
  {
    "path": "prometheus/metrics_set.go",
    "chars": 1194,
    "preview": "//go:build !noprom\n// +build !noprom\n\npackage prometheus\n\nimport (\n\t\"github.com/prometheus/prometheus/model/labels\"\n\t\"gi"
  },
  {
    "path": "prometheus/querier.go",
    "chars": 3248,
    "preview": "//go:build !noprom\n// +build !noprom\n\npackage prometheus\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pr"
  },
  {
    "path": "prometheus/querier_select.go",
    "chars": 3680,
    "preview": "//go:build !noprom\n// +build !noprom\n\npackage prometheus\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-click"
  },
  {
    "path": "prometheus/querier_select_test.go",
    "chars": 2417,
    "preview": "//go:build !noprom\n// +build !noprom\n\npackage prometheus\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-click"
  },
  {
    "path": "prometheus/run.go",
    "chars": 2603,
    "preview": "//go:build !noprom\n// +build !noprom\n\npackage prometheus\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/g"
  },
  {
    "path": "prometheus/run_dummy.go",
    "chars": 169,
    "preview": "//go:build noprom\n// +build noprom\n\npackage prometheus\n\nimport (\n\t\"github.com/lomik/graphite-clickhouse/config\"\n)\n\nfunc "
  },
  {
    "path": "prometheus/series_set.go",
    "chars": 5343,
    "preview": "//go:build !noprom\n// +build !noprom\n\npackage prometheus\n\nimport (\n\t\"log\"\n\t\"math\"\n\n\t\"github.com/prometheus/prometheus/ut"
  },
  {
    "path": "prometheus/storage.go",
    "chars": 942,
    "preview": "//go:build !noprom\n// +build !noprom\n\npackage prometheus\n\nimport (\n\t\"context\"\n\n\t\"github.com/lomik/graphite-clickhouse/co"
  },
  {
    "path": "render/data/carbonlink.go",
    "chars": 2045,
    "preview": "package data\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n\t\"github.com/lomik/graphite-cl"
  },
  {
    "path": "render/data/carbonlink_test.go",
    "chars": 2625,
    "preview": "package data\n\nimport (\n\t\"context\"\n\t\"sort\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n\t\"github.co"
  },
  {
    "path": "render/data/ch_response.go",
    "chars": 6039,
    "preview": "package data\n\nimport (\n\t\"errors\"\n\t\"math\"\n\n\tv2pb \"github.com/go-graphite/protocol/carbonapi_v2_pb\"\n\tv3pb \"github.com/go-g"
  },
  {
    "path": "render/data/common_step.go",
    "chars": 1314,
    "preview": "package data\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhouse/pkg/dry\"\n)\n\n// This is used to"
  },
  {
    "path": "render/data/common_step_test.go",
    "chars": 1354,
    "preview": "package data\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype wrapper struct {\n\t*commonStep\n\t"
  },
  {
    "path": "render/data/data.go",
    "chars": 9391,
    "preview": "package data\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.c"
  },
  {
    "path": "render/data/data_parse_test.go",
    "chars": 13304,
    "preview": "package data\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhouse/helpe"
  },
  {
    "path": "render/data/multi_target.go",
    "chars": 5843,
    "preview": "package data\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\tv3pb \"github.com/go-graphite/protocol/carbonapi_v"
  },
  {
    "path": "render/data/multi_target_test.go",
    "chars": 3141,
    "preview": "package data\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n)\n\nfunc Test_getDataTimeout(t "
  },
  {
    "path": "render/data/query.go",
    "chars": 12276,
    "preview": "package data\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"t"
  },
  {
    "path": "render/data/query_test.go",
    "chars": 16559,
    "preview": "package data\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\tv3pb \"github.com/go-graphite/protocol/carb"
  },
  {
    "path": "render/data/targets.go",
    "chars": 4775,
    "preview": "package data\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\tv3pb \"github.com/go-graphite/protocol/carbonapi_v3_pb\"\n\t\"github.com/lomik/graphi"
  },
  {
    "path": "render/data/targets_test.go",
    "chars": 3226,
    "preview": "package data\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n\t\"github.com/stretchr/t"
  },
  {
    "path": "render/handler.go",
    "chars": 10667,
    "preview": "package render\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"go.uber.org/zap\"\n\n\t\"github.com/go-g"
  },
  {
    "path": "render/handler_test.go",
    "chars": 1935,
    "preview": "package render\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhouse/config\"\n)\n\nfunc Test_getCache"
  },
  {
    "path": "render/reply/formatter.go",
    "chars": 2314,
    "preview": "package reply\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/lomik/graphite-clickhouse/pkg/alias\"\n\t\"githu"
  },
  {
    "path": "render/reply/formatter_test.go",
    "chars": 6373,
    "preview": "package reply\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.co"
  },
  {
    "path": "render/reply/json.go",
    "chars": 3541,
    "preview": "package reply\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"net/http\"\n\n\tv3pb \"github.com/go-graphite/pr"
  },
  {
    "path": "render/reply/pickle.go",
    "chars": 3460,
    "preview": "package reply\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"net/http\"\n\t\"time\"\n\n\tgraphitePickle \"github.com/lomik/graphit"
  },
  {
    "path": "render/reply/protobuf.go",
    "chars": 4072,
    "preview": "package reply\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"net/http\"\n\n\t\"go.uber.org/zap\"\n\n\t\"github.com/lomik/graph"
  },
  {
    "path": "render/reply/protobuf_test.go",
    "chars": 267,
    "preview": "package reply\n\nimport (\n\t\"encoding/binary\"\n\t\"testing\"\n)\n\nfunc TestVarintLen(t *testing.T) {\n\tbuf := make([]byte, binary."
  },
  {
    "path": "render/reply/v2_pb.go",
    "chars": 2712,
    "preview": "package reply\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"net/http\"\n\n\t\"github.com/lomik/graphite-clickhouse/h"
  },
  {
    "path": "render/reply/v2_pb_test.go",
    "chars": 2813,
    "preview": "package reply\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"math\"\n\t\"reflect\"\n\t\"testing\"\n\n\tv2pb \"github.com/go-graphite/protocol/carbonap"
  },
  {
    "path": "render/reply/v3_pb.go",
    "chars": 4435,
    "preview": "package reply\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\tv3pb \"github.com/go-grap"
  },
  {
    "path": "render/reply/v3_pb_test.go",
    "chars": 3373,
    "preview": "package reply\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"math\"\n\t\"reflect\"\n\t\"testing\"\n\n\tv3pb \"github.com/go-graphite/protocol/carbonap"
  },
  {
    "path": "sd/nginx/nginx.go",
    "chars": 8965,
    "preview": "package nginx\n\nimport (\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/lomik/graphite-clickhou"
  }
]

// ... and 93 more files (download for full content)

About this extraction

This page contains the full source code of the lomik/graphite-clickhouse GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 293 files (1.3 MB), approximately 455.3k tokens, and a symbol index with 1236 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!