├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── 01_BUG_REPORT.md │ ├── 02_FEATURE_REQUEST.md │ └── 03_CODEBASE_IMPROVEMENT.md └── workflows │ ├── lint.yml │ └── tests.yml ├── .gitignore ├── .golangci.yml ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── chart ├── .helmignore ├── Chart.yaml ├── README.md ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── agent-ds.yaml │ ├── agent-svc.yaml │ ├── collector-deploy.yaml │ ├── collector-svc.yaml │ ├── query-deploy.yaml │ ├── query-svc.yaml │ ├── secret.yaml │ ├── serviceaccount.yaml │ ├── watcher-deploy.yaml │ └── ydb-environment.tpl └── values.yaml ├── cmd └── schema │ ├── cmd.go │ └── watcher │ ├── lru.go │ ├── tables.go │ └── watcher.go ├── docker-compose.example.yml ├── examples ├── README.md ├── docker-compose.dedicated.yaml └── docker-compose.serverless.yaml ├── go.mod ├── go.sum ├── internal ├── db │ ├── credentials_types.go │ ├── dialer.go │ ├── dialer_test.go │ ├── errors.go │ └── viper.go ├── testutil │ ├── db_helper.go │ ├── logger.go │ └── traces.go └── viper │ └── viper.go ├── main.go ├── plugin ├── metrics.go └── plugin.go ├── schema ├── db.go ├── partition.go ├── partition_test.go ├── queries.go └── schema.go └── storage ├── config └── options.go ├── dependencystore └── storage.go └── spanstore ├── batch ├── batch.go └── queue.go ├── dbmodel ├── hash.go ├── index.go ├── model.go ├── model_test.go ├── model_ydb.go ├── proto │ ├── model.proto │ └── spandata.proto ├── spandata.pb.go ├── unique_ids.go └── unique_ids_test.go ├── indexer ├── bucket.go ├── bucket_test.go ├── index │ ├── base.go │ ├── idx_duration.go │ ├── idx_operation.go │ ├── idx_service.go │ ├── idx_tag.go │ ├── trace_ids.go │ └── trace_ids_test.go ├── indexer.go ├── options.go ├── rand.go ├── tag_helper.go ├── ttl_map.go └── writer.go ├── queries └── reader_queries.go ├── reader ├── archive_reader_test.go ├── cache.go ├── helpers.go ├── reader.go └── reader_test.go └── writer ├── archive_writer.go ├── archive_writer_test.go ├── batch_writer.go ├── metrics.go ├── metrics └── metrics.go ├── options.go ├── writer.go └── writer_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.git 3 | /Makefile 4 | /ydb_data 5 | /ydb_certs -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01_BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help jaeger-ydb-store to improve 4 | title: "bug: " 5 | labels: "bug" 6 | assignees: "" 7 | 8 | --- 9 | 10 | # Bug Report 11 | 12 | **jaeger-ydb-store version:** 13 | 14 | 15 | 16 | **Environment** 17 | 18 | 19 | 20 | **Current behavior:** 21 | 22 | 23 | 24 | **Expected behavior:** 25 | 26 | 27 | 28 | **Steps to reproduce:** 29 | 30 | 31 | 32 | **Related code:** 33 | 34 | 35 | 36 | ``` 37 | insert short code snippets here 38 | ``` 39 | 40 | **Other information:** 41 | 42 | 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02_FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: "feat: " 5 | labels: "enhancement" 6 | assignees: "" 7 | 8 | --- 9 | 10 | # Feature Request 11 | 12 | **Describe the Feature Request** 13 | 14 | 15 | 16 | **Describe Preferred Solution** 17 | 18 | 19 | 20 | **Describe Alternatives** 21 | 22 | 23 | 24 | **Related Code** 25 | 26 | 27 | 28 | **Additional Context** 29 | 30 | 31 | 32 | **If the feature request is approved, would you be willing to submit a PR?** 33 | Yes / No _(Help can be provided if you need assistance submitting a PR)_ 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/03_CODEBASE_IMPROVEMENT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Codebase improvement 3 | about: Provide your feedback for the existing codebase. Suggest a better solution for algorithms, development tools, etc. 4 | title: "dev: " 5 | labels: "enhancement" 6 | assignees: "" 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | branches: 7 | - master 8 | - '**' 9 | pull_request: 10 | workflow_dispatch: 11 | jobs: 12 | golangci: 13 | name: golangci-lint 14 | concurrency: 15 | group: lint-golangci-${{ github.ref }} 16 | cancel-in-progress: true 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v3 22 | with: 23 | version: v1.52.0 -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | - '**' 9 | pull_request: 10 | workflow_dispatch: 11 | jobs: 12 | integration: 13 | strategy: 14 | matrix: 15 | go-version: [1.21.x, 1.22.x] 16 | os: [ubuntu-latest] 17 | concurrency: 18 | group: integration-${{ github.ref }}-${{ matrix.os }}-${{ matrix.go-version }} 19 | cancel-in-progress: true 20 | services: 21 | ydb: 22 | image: cr.yandex/yc/yandex-docker-local-ydb:latest 23 | ports: 24 | - 2135:2135 25 | volumes: 26 | - /tmp/ydb_certs:/ydb_certs 27 | env: 28 | YDB_LOCAL_SURVIVE_RESTART: true 29 | YDB_USE_IN_MEMORY_PDISKS: true 30 | options: '-h localhost' 31 | env: 32 | OS: ${{ matrix.os }} 33 | GO: ${{ matrix.go-version }} 34 | YDB_ADDRESS: localhost:2135 35 | YDB_PATH: /local 36 | YDB_FOLDER: jaeger 37 | YDB_TOKEN: "" 38 | YDB_SECURE: 1 39 | YDB_SSL_ROOT_CERTIFICATES_FILE: /tmp/ydb_certs/ca.pem 40 | runs-on: ${{ matrix.os }} 41 | steps: 42 | - name: Install Go 43 | uses: actions/setup-go@v2 44 | with: 45 | go-version: ${{ matrix.go-version }} 46 | - name: Checkout code 47 | uses: actions/checkout@v2 48 | - name: Test 49 | run: go test -race -coverpkg=./... -coverprofile=integration.txt -covermode=atomic ./... 50 | - name: Upload coverage to Codecov 51 | uses: codecov/codecov-action@v2 52 | with: 53 | file: ./integration.txt 54 | flags: integration,${{ matrix.os }},${{ matrix.go-version }} 55 | name: integration -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | .*.swp 3 | vendor 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # options for analysis running 2 | run: 3 | # default concurrency is a available CPU number 4 | concurrency: 4 5 | 6 | # timeout for analysis, e.g. 30s, 5m, default is 1m 7 | deadline: 5m 8 | 9 | # exit code when at least one issue was found, default is 1 10 | issues-exit-code: 1 11 | 12 | # include test files or not, default is true 13 | tests: true 14 | 15 | # list of build tags, all linters use it. Default is empty list. 16 | #build-tags: 17 | # - mytag 18 | 19 | # which dirs to skip: they won't be analyzed; 20 | # can use regexp here: generated.*, regexp is applied on full path; 21 | # default value is empty list, but next dirs are always skipped independently 22 | # from this option's value: 23 | # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ 24 | # skip-dirs: 25 | 26 | # which files to skip: they will be analyzed, but issues from them 27 | # won't be reported. Default value is empty list, but there is 28 | # no need to include all autogenerated files, we confidently recognize 29 | # autogenerated files. If it's not please let us know. 30 | # skip-files: 31 | # - ".*\\.my\\.go$" 32 | # - lib/bad.go 33 | 34 | 35 | # output configuration options 36 | output: 37 | # colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number" 38 | format: colored-line-number 39 | 40 | # print lines of code with issue, default is true 41 | print-issued-lines: true 42 | 43 | # print linter name in the end of issue text, default is true 44 | print-linter-name: true 45 | 46 | 47 | # all available settings of specific linters 48 | linters-settings: 49 | errcheck: 50 | # report about not checking of errors in types assetions: `a := b.(MyStruct)`; 51 | # default is false: such cases aren't reported by default. 52 | check-type-assertions: false 53 | 54 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; 55 | # default is false: such cases aren't reported by default. 56 | check-blank: false 57 | govet: 58 | # report about shadowed variables 59 | check-shadowing: true 60 | fieldalignment: true 61 | golint: 62 | # minimal confidence for issues, default is 0.8 63 | min-confidence: 0.8 64 | gofmt: 65 | # simplify code: gofmt with `-s` option, true by default 66 | simplify: true 67 | goimports: 68 | # put imports beginning with prefix after 3rd-party packages; 69 | # it's a comma-separated list of prefixes 70 | local-prefixes: github.com/ydb-platform/jaeger-ydb-store 71 | goconst: 72 | # minimal length of string constant, 3 by default 73 | min-len: 2 74 | # minimal occurrences count to trigger, 3 by default 75 | min-occurrences: 2 76 | fieldalignment: 77 | # print struct with more effective memory layout or not, false by default 78 | suggest-new: true 79 | misspell: 80 | # Correct spellings using locale preferences for US or UK. 81 | # Default is to use a neutral variety of English. 82 | # Setting locale to US will correct the British spelling of 'colour' to 'color'. 83 | locale: US 84 | ignore-words: 85 | - cancelled 86 | revive: 87 | rules: 88 | - name: blank-imports 89 | - name: context-as-argument 90 | - name: context-keys-type 91 | - name: dot-imports 92 | - name: error-return 93 | - name: error-strings 94 | - name: error-naming 95 | - name: exported 96 | - name: if-return 97 | - name: increment-decrement 98 | - name: var-naming 99 | - name: var-declaration 100 | - name: package-comments 101 | - name: range 102 | - name: receiver-naming 103 | - name: time-naming 104 | - name: indent-error-flow 105 | - name: errorf 106 | - name: empty-block 107 | - name: superfluous-else 108 | - name: unreachable-code 109 | unused: 110 | # treat code as a program (not a library) and report unused exported identifiers; default is false. 111 | # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: 112 | # if it's called for subdir of a project it can't find funcs usages. All text editor integrations 113 | # with golangci-lint call it on a directory with the changed file. 114 | check-exported: false 115 | unparam: 116 | # call graph construction algorithm (cha, rta). In general, use cha for libraries, 117 | # and rta for programs with main packages. Default is cha. 118 | algo: cha 119 | 120 | # Inspect exported functions, default is false. Set to true if no external program/library imports your code. 121 | # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: 122 | # if it's called for subdir of a project it can't find external interfaces. All text editor integrations 123 | # with golangci-lint call it on a directory with the changed file. 124 | check-exported: false 125 | 126 | linters: 127 | disable-all: true 128 | enable: 129 | # - cyclop 130 | - depguard 131 | - dogsled 132 | # - dupl 133 | # - errcheck // TODO: fix issues 134 | # - errorlint // TODO: fix issues 135 | # - exhaustive 136 | # - exhaustivestruct 137 | # - forbidigo 138 | # - funlen 139 | # - gci 140 | # - gocognit 141 | - goconst 142 | # - gocritic // TODO: fix issues 143 | - gocyclo 144 | # - godot 145 | # - godox// TODO: fix issues 146 | - gofmt # On why gofmt when goimports is enabled - https://github.com/golang/go/issues/21476 147 | - gofumpt 148 | - goheader 149 | - goimports 150 | # - gomnd 151 | # - gomoddirectives 152 | # - gomodguard 153 | # - gosec // TODO: fix issues 154 | # - gosimple // TODO: fix issues 155 | # - govet // TODO: fix issues 156 | - depguard 157 | # - ifshort 158 | # - ireturn 159 | # - lll // TODO: fix issues 160 | - makezero 161 | # - maligned // TODO: fix issues 162 | - misspell 163 | - ineffassign 164 | - misspell 165 | - nakedret 166 | # - nestif // TODO: fix issues 167 | # - nilnil 168 | # - nlreturn 169 | - nolintlint 170 | # - prealloc // TODO: fix issues 171 | - predeclared 172 | - rowserrcheck 173 | # - revive // TODO: fix issues 174 | - staticcheck 175 | # - stylecheck // TODO: fix issues 176 | # - tagliatelle 177 | # - testpackage 178 | # - thelper 179 | # - tenv 180 | - typecheck 181 | # - unconvert // TODO: fix issues 182 | - unparam 183 | - unused 184 | # - varnamelen 185 | - whitespace 186 | # - wrapcheck 187 | # - wsl 188 | 189 | issues: 190 | # List of regexps of issue texts to exclude, empty list by default. 191 | # But independently from this option we use default exclude patterns, 192 | # it can be disabled by `exclude-use-default: false`. To list all 193 | # excluded by default patterns execute `golangci-lint run --help` 194 | # exclude: 195 | 196 | # List of regexps of issue texts to exclude. 197 | # 198 | # But independently of this option we use default exclude patterns, 199 | # it can be disabled by `exclude-use-default: false`. 200 | # To list all excluded by default patterns execute `golangci-lint run --help` 201 | # 202 | # Default: [] 203 | 204 | # Independently from option `exclude` we use default exclude patterns, 205 | # it can be disabled by this option. To list all 206 | # excluded by default patterns execute `golangci-lint run --help`. 207 | # Default value for this option is true. 208 | exclude-use-default: true 209 | 210 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 211 | max-per-linter: 0 212 | 213 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 214 | max-same-issues: 0 215 | 216 | # Show only new issues: if there are unstaged changes or untracked files, 217 | # only those changes are analyzed, else only changes in HEAD~ are analyzed. 218 | # It's a super-useful option for integration of golangci-lint into existing 219 | # large codebase. It's not practical to fix all existing issues at the moment 220 | # of integration: much better don't allow issues in new code. 221 | # Default is false. 222 | new: false 223 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | The following authors have created the source code of "Jaeger YDB Plugin" published and distributed by YANDEX LLC as the owner: 2 | 3 | Alexander Saltykov 4 | 5 | The following authors have licensed their contributions to YANDEX LLC and everyone who uses "Jaeger YDB Plugin" under the licensing terms detailed in LICENSE available at https://github.com/ydb-platform/jaeger-ydb-store/blob/master/LICENSE. 6 | 7 | Sergey Dubovtsev 8 | Aleksandr Shcherbakov 9 | Andrew Putilov 10 | Nazim Malyshev 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes by version 2 | 3 | 1.9.1 (2024-02-15) 4 | ------------------ 5 | 6 | * updated jaeger to v1.54.0 7 | * updated ydb-go-sdk to v3.56.1 8 | 9 | 1.9.0 (2023-09-11) 10 | ------------------ 11 | 12 | * updated jaeger to v1.47 13 | * added ability to configure connection secure type `ydb.secure-connection` 14 | * added new credentials types: `SaKeyJson` and `Anonymous` 15 | * now `SaId`, `SaKeyId` and `SaPrivateKeyFile` have become deprecated - use `SaKeyJson` insted 16 | * added the ability to configure write attempt time 17 | * added graceful shutdown 18 | 19 | 1.8.3 (2023-08-09) 20 | ------------------ 21 | 22 | * updated golang/protobuf to v1.5.3 23 | * updated hashicorp/go-hclog to v1.5.0 24 | * updated stretchr/testify to v1.8.3 25 | * updated ydb-platform/ydb-go-sdk-zap to v0.16.1 26 | * updated ydb-platform/ydb-go-sdk/v3 to v3.48.7 27 | * updated go.uber.org/zap to v1.24.0 28 | * updated google.golang.org/grpc to v1.56.2 29 | * fixed bug with collector (issue [#49](https://github.com/ydb-platform/jaeger-ydb-store/issues/49)) 30 | * added logging that can be seen in the jaeger (issue [#51](https://github.com/ydb-platform/jaeger-ydb-store/issues/51)) 31 | * refactored code 32 | * removed extra logging from scheme watcher 33 | * changed the Dockerfile for faster builds 34 | 35 | 1.8.2 (2022-06-10) 36 | ------------------ 37 | 38 | * updated ydb-go-sdk to v3.26.10 39 | * updated ydb-go-sdk-zap to v0.10.1 40 | * updated ydb-go-yc to v0.9.0 41 | * updated grpc to v1.45.0 42 | * added gofumpt and goimports linters to ckeck source code and fixed issues from them 43 | 44 | 1.8.1 (2022-03-25) 45 | ------------------ 46 | 47 | * updated ydb-go-sdk to v3.16.11 48 | 49 | 1.8.0 (2022-03-02) 50 | ------------------ 51 | 52 | * updated ydb-go-sdk to v3.11.11 53 | * add ydb sdk logger 54 | 55 | 1.7.0 (2022-01-03) 56 | ------------------ 57 | 58 | * updated ydb-go-sdk from v3.4.1 to v3.7.0 59 | * updated ydb-go-yc from v0.2.1 to v0.4.2 60 | * fixed code with latest ydb-go-sdk 61 | * added integration tests instead docker with ydb 62 | * added issue templates 63 | * fixed some linter issues 64 | 65 | 1.6.0 (2021-12-24) 66 | ------------------ 67 | 68 | * update ydb-go-sdk to v3 69 | * increase num results limit for querying service names 70 | * add config flag for reading config file 71 | * fix initial partition size on table schema creation 72 | 73 | 1.5.1 (2021-04-28) 74 | ------------------ 75 | 76 | ### Improvements 77 | * increase query limit for op names 78 | 79 | 1.5.0 (2021-04-28) 80 | ------------------ 81 | 82 | ### Changes 83 | * Updated [README](README.md) 84 | * Updated Go to 1.16.3 85 | * Added YDB feature flags (see [README](README.md) for description) for Watcher 86 | - `YDB_FEATURE_SPLIT_BY_LOAD` 87 | - `YDB_FEATURE_COMPRESSION` 88 | 89 | ### Improvements 90 | * Started using `OnlineReadOnly` + `AllowInconsistentReads` in Query 91 | instead of `SerializableReadWrite` isolation level 92 | * Added `DISTINCT` to a couple of search queries 93 | 94 | 1.4.3 (2021-04-21) 95 | ------------------ 96 | 97 | ### Changes 98 | * Skip outdated spans 99 | 100 | 1.4.2 (2021-02-18) 101 | ------------------ 102 | 103 | ### Changes 104 | * span writer: svc+op cache size 105 | 106 | 1.4.1 (2020-12-08) 107 | ------------------ 108 | 109 | ### Changes 110 | * schema watcher: cache created tables 111 | 112 | 1.4.0 (2020-09-30) 113 | ------------------ 114 | 115 | ### Changes 116 | * add archive storage support 117 | * bump jaeger base image to 1.20.0 118 | 119 | 1.3.1 (2020-07-10) 120 | ------------------ 121 | 122 | ### Changes 123 | * idx_tag_v2 schema watcher defaults 124 | * remove old idx_tag schema from creation 125 | * don't write batch overflow error to log 126 | * bump golang to 1.14.4 127 | * bump jaeger base image to 1.18.1 128 | 129 | 1.3.0 (2020-07-10) 130 | ------------------ 131 | 132 | ### Breaking changes 133 | * idx_tag_v2: reduce number of index records, not compatible with old dataset 134 | 135 | ### Improvements 136 | * configurable threadpool for FindTraces query 137 | * configurable number of daily partitioned tables 138 | 139 | 1.2.0 (2020-04-16) 140 | ------------------ 141 | 142 | ### Breaking changes 143 | * update for jaeger 1.17, change operation_names index to support client/server spans 144 | 145 | ### Improvements 146 | * update ydb sdk to fix bad sessions leaking 147 | * update IAM client 148 | * use P2C balancing method for ydb client 149 | 150 | 151 | 1.1.0 (2020-02-14) 152 | ------------------ 153 | 154 | 155 | ### Breaking changes 156 | * tag indexer: index service+operation_name+tag in addition to service+tag, breaks searching through old dataset 157 | 158 | 1.0.0 (2020-02-06) 159 | ------------------ 160 | * Initial release 161 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Notice to external contributors 2 | 3 | 4 | ## General info 5 | 6 | Hello! In order for us (YANDEX LLC) to accept patches and other contributions from you, you will have to adopt our Yandex Contributor License Agreement (the “**CLA**”). The current version of the CLA can be found here: 7 | 1) https://yandex.ru/legal/cla/?lang=en (in English) and 8 | 2) https://yandex.ru/legal/cla/?lang=ru (in Russian). 9 | 10 | By adopting the CLA, you state the following: 11 | 12 | * You obviously wish and are willingly licensing your contributions to us for our open source projects under the terms of the CLA, 13 | * You have read the terms and conditions of the CLA and agree with them in full, 14 | * You are legally able to provide and license your contributions as stated, 15 | * We may use your contributions for our open source projects and for any other our project too, 16 | * We rely on your assurances concerning the rights of third parties in relation to your contributions. 17 | 18 | If you agree with these principles, please read and adopt our CLA. By providing us your contributions, you hereby declare that you have already read and adopt our CLA, and we may freely merge your contributions with our corresponding open source project and use it in further in accordance with terms and conditions of the CLA. 19 | 20 | ## Provide contributions 21 | 22 | If you have already adopted terms and conditions of the CLA, you are able to provide your contributions. When you submit your pull request, please add the following information into it: 23 | 24 | ``` 25 | I hereby agree to the terms of the CLA available at: [link]. 26 | ``` 27 | 28 | Replace the bracketed text as follows: 29 | * [link] is the link to the current version of the CLA: https://yandex.ru/legal/cla/?lang=en (in English) or https://yandex.ru/legal/cla/?lang=ru (in Russian). 30 | 31 | It is enough to provide us such notification once. 32 | 33 | ## Other questions 34 | 35 | If you have any questions, please mail us at opensource@yandex-team.ru. 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG jaeger_version=1.47.0 2 | ARG golang_version=1.20 3 | ARG alpine_version=3.10 4 | 5 | FROM jaegertracing/jaeger-collector:${jaeger_version} as base-collector 6 | 7 | FROM jaegertracing/jaeger-query:${jaeger_version} as base-query 8 | 9 | FROM golang:${golang_version} as builder 10 | WORKDIR /build 11 | COPY go.mod go.sum ./ 12 | RUN go mod download && go mod verify 13 | COPY . . 14 | RUN CGO_ENABLED=0 go build -ldflags='-w -s' -o /ydb-plugin . 15 | RUN CGO_ENABLED=0 go build -ldflags='-w -s' -o /ydb-schema ./cmd/schema 16 | 17 | FROM alpine:${alpine_version} AS watcher 18 | ENV YDB_CA_FILE="/ydb-ca.pem" 19 | RUN apk add --no-cache ca-certificates && \ 20 | wget "https://storage.yandexcloud.net/cloud-certs/CA.pem" -O /ydb-ca.pem 21 | COPY --from=builder /ydb-schema / 22 | ENTRYPOINT ["/ydb-schema"] 23 | 24 | FROM alpine:${alpine_version} AS shared 25 | ENV SPAN_STORAGE_TYPE="grpc-plugin" 26 | ENV GRPC_STORAGE_PLUGIN_BINARY="/ydb-plugin" 27 | ENV YDB_CA_FILE="/ydb-ca.pem" 28 | RUN apk add --no-cache ca-certificates && \ 29 | wget "https://storage.yandexcloud.net/cloud-certs/CA.pem" -O /ydb-ca.pem 30 | COPY --from=builder /ydb-plugin / 31 | 32 | FROM shared AS collector 33 | COPY --from=base-collector /go/bin/collector-linux /jaeger-collector 34 | EXPOSE 9411 35 | EXPOSE 14250 36 | EXPOSE 14268 37 | EXPOSE 14269 38 | EXPOSE 4317 39 | EXPOSE 4318 40 | ENTRYPOINT ["/jaeger-collector"] 41 | 42 | FROM shared AS query 43 | COPY --from=base-query /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 44 | COPY --from=base-query /go/bin/query-linux /jaeger-query 45 | EXPOSE 16685 46 | EXPOSE 16686 47 | EXPOSE 16687 48 | ENTRYPOINT ["/jaeger-query"] 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 YANDEX LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOCKER_NAMESPACE ?= cr.yandex/yc 2 | DOCKER_TAG ?= dev 3 | 4 | none: 5 | $(error no target specified) 6 | 7 | image-collector: 8 | docker build -t $(DOCKER_NAMESPACE)/jaeger-ydb-collector:$(DOCKER_TAG) --target collector . 9 | 10 | image-query: 11 | docker build -t $(DOCKER_NAMESPACE)/jaeger-ydb-query:$(DOCKER_TAG) --target query . 12 | 13 | image-watcher: 14 | docker build -t $(DOCKER_NAMESPACE)/jaeger-ydb-watcher:$(DOCKER_TAG) --target watcher . 15 | 16 | images: image-watcher image-collector image-query 17 | 18 | push-images: 19 | docker push $(DOCKER_NAMESPACE)/jaeger-ydb-collector:$(DOCKER_TAG) 20 | docker push $(DOCKER_NAMESPACE)/jaeger-ydb-query:$(DOCKER_TAG) 21 | docker push $(DOCKER_NAMESPACE)/jaeger-ydb-watcher:$(DOCKER_TAG) 22 | 23 | generate: 24 | go generate ./... 25 | 26 | PROTOC := "protoc" 27 | PROTO_INCLUDES := \ 28 | -I storage/spanstore/dbmodel/proto \ 29 | -I vendor/github.com/gogo/googleapis \ 30 | -I vendor/github.com/gogo/protobuf 31 | PROTO_GOGO_MAPPINGS := $(shell echo \ 32 | Mgoogle/protobuf/descriptor.proto=github.com/gogo/protobuf/types, \ 33 | Mgoogle/protobuf/timestamp.proto=github.com/gogo/protobuf/types, \ 34 | Mgoogle/protobuf/duration.proto=github.com/gogo/protobuf/types, \ 35 | Mgoogle/protobuf/empty.proto=github.com/gogo/protobuf/types, \ 36 | Mgoogle/api/annotations.proto=github.com/gogo/googleapis/google/api, \ 37 | Mmodel.proto=github.com/jaegertracing/jaeger/model \ 38 | | sed 's/ //g') 39 | 40 | proto: 41 | $(PROTOC) \ 42 | $(PROTO_INCLUDES) \ 43 | --gofast_out=plugins=grpc,$(PROTO_GOGO_MAPPINGS):storage/spanstore/dbmodel \ 44 | storage/spanstore/dbmodel/proto/spandata.proto 45 | 46 | .PHONY: image-collector image-query images 47 | -------------------------------------------------------------------------------- /chart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: jaeger-ydb-store 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.2.7 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: 1.9.1 25 | -------------------------------------------------------------------------------- /chart/README.md: -------------------------------------------------------------------------------- 1 | # YDB storage plugin for Jaeger 2 | 3 | ## Introduction 4 | This chart adds all components required to run [Jaeger](https://github.com/jaegertracing/jaeger) using [Yandex.Database](https://cloud.yandex.ru/services/ydb) backend storage. Chart will deploy jaeger-agent as a DaemonSet and deploy the jaeger-collector, jaeger-query and schema-watcher components as Deployments. 5 | 6 | ## Configuration 7 | ### Storage 8 | You should create dedicated Yandex Database as described in [YDB documentation](https://cloud.yandex.ru/docs/ydb/quickstart/create-db) before installing this chart. After creating database you will get YDB Endpoint and database name needed to create Jaeger store: 9 | ```qoute 10 | endpoint: grpcs://lb.etns9ff54e1j4d7.ydb.mdb.yandexcloud.net:2135/?database=/ru-central1/afg8slkos03mal53s/etns9ff54e1j4d7 11 | ``` 12 | 13 | ### Parameters 14 | This is necessary parameters, all other options described in [YDB storage plugin for Jaeger documentation](https://github.com/ydb-platform/jaeger-ydb-store#environment-variables) and can be overriden using `ydb.env.{ENV_VARIABLE}` 15 | 16 | | Name | Type | Default | Description | 17 | |-------------------|----------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------| 18 | | `ydb.endpoint` | `string` | | db endpoint host:port to connect to | 19 | | `ydb.useMetaAuth` | `bool` | `false` | use metadata to authorize requests [documentation](https://cloud.yandex.com/docs/compute/operations/vm-connect/auth-inside-vm#auth-inside-vm) | 20 | | `ydb.saId` | `string` | | service account id for Yandex.Cloud authorization [documentation on service accounts](https://cloud.yandex.com/docs/iam/concepts/users/service-accounts) | 21 | | `ydb.saKeyId` | `string` | | service account key id for Yandex.Cloud authorization | 22 | | `saPrivateKey` | `string` | | service account private key for Yandex.Cloud authorization | 23 | | `ydb.database` | `string` | | database name | 24 | | `ydb.folder` | `string` | `jaeger` | folder for tables to store data in | 25 | 26 | ## Installing the Chart 27 | Add the Jaeger Tracing Helm repository: 28 | ```bash 29 | $ helm repo add jaeger-ydb-store https://charts.ydb.tech/ 30 | $ helm repo update 31 | ``` 32 | To install a release named jaeger: 33 | ```bash 34 | $ helm install jaeger jaeger-ydb-store/jaeger-ydb-store \ 35 | --set ydb.endpoint={YDB_ENDPOINT}}:2135 \ 36 | --set ydb.database={YDB_DATABASE}} \ 37 | --set ydb.folder={YDB_FOLDER}} \ 38 | --set ydb.useMetaAuth=true 39 | ``` 40 | 41 | or using Service Account Key: 42 | ```bash 43 | $ helm install jaeger jaeger-ydb-store/jaeger-ydb-store \ 44 | --set ydb.endpoint={YDB_ENDPOINT}}:2135 \ 45 | --set ydb.database={YDB_DATABASE}} \ 46 | --set ydb.folder={YDB_FOLDER}} \ 47 | --set ydb.saId={SA_ID}} \ 48 | --set ydb.saKeyId={SA_KEY_ID}} \ 49 | --set ydb.saPrivateKey="$(cat ~/jaeger-over-ydb-sa-key.pem)" 50 | ``` 51 | 52 | **Use either Metadata or Service Account Key authorization** 53 | 54 | Alternatively, a YAML file that specifies the values for the parameters can be provided while installing the chart. For example, 55 | ```bash 56 | $ helm install jaeger jaeger-ydb-store/jaeger-ydb-store --values values.yaml 57 | ``` 58 | You can get default values.yaml from [chart repository](https://github.com/ydb-platform/jaeger-ydb-store/chart/values.yaml). 59 | 60 | By default, the chart deploys the following: 61 | 62 | - Jaeger Agent DaemonSet 63 | - Jaeger Collector Deployment 64 | - Jaeger Query (UI) Deployment 65 | - Schema-Watcher Deployment (creates new tables for spans/indexes and removes old ones) 66 | 67 | You can use Jaeger Agent as a sidecar for you service and not to deploy as DaemonSet by setting `--set agent.enable=false` 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /chart/templates/NOTES.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ydb-platform/jaeger-ydb-store/1c2d697d4f4c95c6954399432662f6e0879bb30d/chart/templates/NOTES.txt -------------------------------------------------------------------------------- /chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "jaeger-ydb-store.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "jaeger-ydb-store.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "jaeger-ydb-store.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "jaeger-ydb-store.labels" -}} 37 | helm.sh/chart: {{ include "jaeger-ydb-store.chart" . }} 38 | {{ include "jaeger-ydb-store.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "jaeger-ydb-store.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "jaeger-ydb-store.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Watcher selector labels 55 | */}} 56 | {{- define "jaeger-ydb-store.watcherSelectorLabels" -}} 57 | jaeger-ydb-store.component: watcher 58 | {{- end }} 59 | 60 | {{/* 61 | Collector selector labels 62 | */}} 63 | {{- define "jaeger-ydb-store.collectorSelectorLabels" -}} 64 | jaeger-ydb-store.component: collector 65 | {{- end }} 66 | 67 | {{/* 68 | Query selector labels 69 | */}} 70 | {{- define "jaeger-ydb-store.querySelectorLabels" -}} 71 | jaeger-ydb-store.component: query 72 | {{- end }} 73 | 74 | {{/* 75 | Agent selector labels 76 | */}} 77 | {{- define "jaeger-ydb-store.agentSelectorLabels" -}} 78 | jaeger-ydb-store.component: agent 79 | {{- end }} 80 | 81 | {{/* 82 | Create the name of the service account to use 83 | */}} 84 | {{- define "jaeger-ydb-store.serviceAccountName" -}} 85 | {{- if .Values.serviceAccount.create }} 86 | {{- default (include "jaeger-ydb-store.fullname" .) .Values.serviceAccount.name }} 87 | {{- else }} 88 | {{- default "default" .Values.serviceAccount.name }} 89 | {{- end }} 90 | {{- end }} 91 | -------------------------------------------------------------------------------- /chart/templates/agent-ds.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.agent.enabled -}} 2 | apiVersion: apps/v1 3 | kind: DaemonSet 4 | metadata: 5 | name: {{ include "jaeger-ydb-store.fullname" . }}-agent 6 | labels: 7 | {{- include "jaeger-ydb-store.labels" . | nindent 4 }} 8 | {{- include "jaeger-ydb-store.agentSelectorLabels" $ | nindent 4 }} 9 | {{- if .Values.agent.annotations }} 10 | annotations: 11 | {{- toYaml .Values.agent.annotations | nindent 4 }} 12 | {{- end }} 13 | spec: 14 | selector: 15 | matchLabels: 16 | {{- include "jaeger-ydb-store.labels" . | nindent 6 }} 17 | {{- include "jaeger-ydb-store.agentSelectorLabels" $ | nindent 6 }} 18 | {{- with .Values.agent.daemonset.updateStrategy }} 19 | updateStrategy: 20 | {{- toYaml . | nindent 4 }} 21 | {{- end }} 22 | template: 23 | metadata: 24 | {{- if .Values.agent.podAnnotations }} 25 | annotations: 26 | {{- toYaml .Values.agent.podAnnotations | nindent 8 }} 27 | {{- end }} 28 | labels: 29 | {{- include "jaeger-ydb-store.labels" . | nindent 8 }} 30 | {{- include "jaeger-ydb-store.agentSelectorLabels" $ | nindent 8 }} 31 | spec: 32 | securityContext: 33 | {{- toYaml .Values.agent.podSecurityContext | nindent 8 }} 34 | {{- if .Values.agent.useHostNetwork }} 35 | hostNetwork: true 36 | {{- end }} 37 | dnsPolicy: {{ .Values.agent.dnsPolicy }} 38 | {{- with .Values.agent.priorityClassName }} 39 | priorityClassName: {{ . }} 40 | {{- end }} 41 | serviceAccountName: {{ include "jaeger-ydb-store.serviceAccountName" . }} 42 | {{- with .Values.imagePullSecrets }} 43 | imagePullSecrets: 44 | {{- toYaml . | nindent 8 }} 45 | {{- end }} 46 | containers: 47 | - name: agent 48 | securityContext: 49 | {{- toYaml .Values.agent.securityContext | nindent 10 }} 50 | image: "{{ .Values.image.agent.repository }}:{{ .Values.image.agent.tag }}" 51 | imagePullPolicy: {{ .Values.image.agent.pullPolicy }} 52 | args: 53 | {{- range $key, $value := .Values.agent.cmdlineParams }} 54 | {{- if $value }} 55 | - --{{ $key }}={{ $value }} 56 | {{- else }} 57 | - --{{ $key }} 58 | {{- end }} 59 | {{- end }} 60 | env: 61 | {{- if .Values.agent.extraEnv }} 62 | {{- toYaml .Values.agent.extraEnv | nindent 10 }} 63 | {{- end }} 64 | {{- if not (hasKey .Values.agent.cmdlineParams "reporter.grpc.host-port") }} 65 | - name: REPORTER_GRPC_HOST_PORT 66 | value: {{ include "jaeger-ydb-store.fullname" $ }}-collector:{{ .Values.service.collector.ports.grpc.port }} 67 | {{- end }} 68 | ports: 69 | - name: admin 70 | containerPort: 14271 71 | protocol: TCP 72 | {{- range $key, $value := merge dict .Values.service.agent.ports }} 73 | - name: {{ $key }} 74 | containerPort: {{ $value.port }} 75 | protocol: {{ default "TCP" $value.protocol }} 76 | {{- if $value.useHostPort }} 77 | hostPort: {{ $value.port }} 78 | {{- end }} 79 | {{- end }} 80 | livenessProbe: 81 | httpGet: 82 | path: / 83 | port: admin 84 | readinessProbe: 85 | httpGet: 86 | path: / 87 | port: admin 88 | resources: 89 | {{- toYaml .Values.resources.agent | nindent 10 }} 90 | volumeMounts: 91 | {{- range .Values.agent.extraConfigmapMounts }} 92 | - name: {{ .name }} 93 | mountPath: {{ .mountPath }} 94 | subPath: {{ .subPath }} 95 | readOnly: {{ .readOnly }} 96 | {{- end }} 97 | {{- range .Values.agent.extraSecretMounts }} 98 | - name: {{ .name }} 99 | mountPath: {{ .mountPath }} 100 | subPath: {{ .subPath }} 101 | readOnly: {{ .readOnly }} 102 | {{- end }} 103 | volumes: 104 | {{- range .Values.agent.extraConfigmapMounts }} 105 | - name: {{ .name }} 106 | configMap: 107 | name: {{ .configMap }} 108 | {{- end }} 109 | {{- range .Values.agent.extraSecretMounts }} 110 | - name: {{ .name }} 111 | secret: 112 | secretName: {{ .secretName }} 113 | {{- end }} 114 | {{- with .Values.agent.nodeSelector }} 115 | nodeSelector: 116 | {{- toYaml . | nindent 8 }} 117 | {{- end }} 118 | {{- with .Values.agent.affinity }} 119 | affinity: 120 | {{- toYaml . | nindent 8 }} 121 | {{- end }} 122 | {{- with .Values.agent.tolerations }} 123 | tolerations: 124 | {{- toYaml . | nindent 8 }} 125 | {{- end }} 126 | {{- end -}} 127 | -------------------------------------------------------------------------------- /chart/templates/agent-svc.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.agent.enabled -}} 2 | {{- with .Values.service.agent }} 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: {{ include "jaeger-ydb-store.fullname" $ }}-agent 7 | {{- with .annotations }} 8 | annotations: 9 | {{- toYaml . | nindent 4 }} 10 | {{- end }} 11 | labels: 12 | {{- include "jaeger-ydb-store.labels" $ | nindent 4 }} 13 | {{- include "jaeger-ydb-store.agentSelectorLabels" $ | nindent 4 }} 14 | spec: 15 | type: {{ .type }} 16 | {{- if .headless }} 17 | clusterIP: None 18 | {{- end }} 19 | {{- with .ipFamilies }} 20 | ipFamilies: 21 | {{- toYaml . | nindent 4 }} 22 | {{- end }} 23 | {{- if .ipFamilyPolicy }} 24 | ipFamilyPolicy: {{ .ipFamilyPolicy }} 25 | {{- end }} 26 | ports: 27 | {{- range $key, $value := .ports }} 28 | - name: {{ $value.name }} 29 | port: {{ $value.port }} 30 | targetPort: {{ $value.name }} 31 | protocol: {{ default "TCP" $value.protocol }} 32 | {{- if $value.nodePort }} 33 | nodePort: {{ $value.nodePort }} 34 | {{- end }} 35 | {{- end }} 36 | selector: 37 | {{- include "jaeger-ydb-store.selectorLabels" $ | nindent 4 }} 38 | {{- include "jaeger-ydb-store.agentSelectorLabels" $ | nindent 4 }} 39 | {{- end }} 40 | {{- end -}} 41 | -------------------------------------------------------------------------------- /chart/templates/collector-deploy.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.collector.enabled -}} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ include "jaeger-ydb-store.fullname" . }}-collector 6 | labels: 7 | {{- include "jaeger-ydb-store.labels" . | nindent 4 }} 8 | {{- include "jaeger-ydb-store.collectorSelectorLabels" $ | nindent 4 }} 9 | spec: 10 | replicas: {{ .Values.collector.replicas }} 11 | selector: 12 | matchLabels: 13 | {{- include "jaeger-ydb-store.selectorLabels" . | nindent 6 }} 14 | {{- include "jaeger-ydb-store.collectorSelectorLabels" $ | nindent 6 }} 15 | template: 16 | metadata: 17 | annotations: 18 | {{- if .Values.ydb.saPrivateKey }} 19 | checksum/secrets: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} 20 | {{- end }} 21 | {{- with .Values.collector.podAnnotations }} 22 | {{- toYaml . | nindent 8 }} 23 | {{- end }} 24 | labels: 25 | {{- include "jaeger-ydb-store.selectorLabels" . | nindent 8 }} 26 | {{- include "jaeger-ydb-store.collectorSelectorLabels" . | nindent 8 }} 27 | spec: 28 | {{- with .Values.imagePullSecrets }} 29 | imagePullSecrets: 30 | {{- toYaml . | nindent 8 }} 31 | {{- end }} 32 | serviceAccountName: {{ include "jaeger-ydb-store.serviceAccountName" . }} 33 | securityContext: 34 | {{- toYaml .Values.collector.podSecurityContext | nindent 8 }} 35 | containers: 36 | - name: collector 37 | env: 38 | {{- include "jaeger-ydb-store.ydb.env" . | nindent 10 }} 39 | securityContext: 40 | {{- toYaml .Values.collector.securityContext | nindent 12 }} 41 | image: "{{ .Values.image.collector.repository }}:{{ .Values.image.collector.tag }}" 42 | imagePullPolicy: {{ .Values.image.collector.pullPolicy }} 43 | livenessProbe: 44 | httpGet: 45 | path: / 46 | port: admin 47 | ports: 48 | - name: admin 49 | containerPort: 14269 50 | protocol: TCP 51 | {{- range $key, $value := merge dict .Values.service.collector.ports }} 52 | - name: {{ $key }} 53 | containerPort: {{ $value.port }} 54 | protocol: {{ default "TCP" $value.protocol }} 55 | {{- end }} 56 | {{- if .Values.ydb.saPrivateKey }} 57 | volumeMounts: 58 | - mountPath: /opt/secrets 59 | name: secrets 60 | readOnly: true 61 | {{- end }} 62 | resources: 63 | {{- toYaml .Values.resources.collector | nindent 12 }} 64 | {{- if .Values.ydb.saPrivateKey }} 65 | volumes: 66 | - name: secrets 67 | secret: 68 | secretName: {{ include "jaeger-ydb-store.fullname" . }} 69 | {{- end }} 70 | {{- with .Values.collector.nodeSelector }} 71 | nodeSelector: 72 | {{- toYaml . | nindent 8 }} 73 | {{- end }} 74 | {{- with .Values.collector.affinity }} 75 | affinity: 76 | {{- toYaml . | nindent 8 }} 77 | {{- end }} 78 | {{- with .Values.collector.tolerations }} 79 | tolerations: 80 | {{- toYaml . | nindent 8 }} 81 | {{- end }} 82 | {{- end -}} 83 | -------------------------------------------------------------------------------- /chart/templates/collector-svc.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.collector.enabled -}} 2 | {{- with .Values.service.collector }} 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: {{ include "jaeger-ydb-store.fullname" $ }}-collector 7 | {{- with .annotations }} 8 | annotations: 9 | {{- toYaml . | nindent 4 }} 10 | {{- end }} 11 | labels: 12 | {{- include "jaeger-ydb-store.labels" $ | nindent 4 }} 13 | {{- include "jaeger-ydb-store.collectorSelectorLabels" $ | nindent 4 }} 14 | spec: 15 | type: {{ .type }} 16 | {{- if .headless }} 17 | clusterIP: None 18 | {{- end }} 19 | {{- with .ipFamilies }} 20 | ipFamilies: 21 | {{- toYaml . | nindent 4 }} 22 | {{- end }} 23 | {{- if .ipFamilyPolicy }} 24 | ipFamilyPolicy: {{ .ipFamilyPolicy }} 25 | {{- end }} 26 | ports: 27 | {{- range $key, $value := .ports }} 28 | - name: {{ $value.name }} 29 | port: {{ $value.port }} 30 | targetPort: {{ $value.name }} 31 | protocol: {{ default "TCP" $value.protocol }} 32 | {{- if $value.nodePort }} 33 | nodePort: {{ $value.nodePort }} 34 | {{- end }} 35 | {{- end }} 36 | selector: 37 | {{- include "jaeger-ydb-store.selectorLabels" $ | nindent 4 }} 38 | {{- include "jaeger-ydb-store.collectorSelectorLabels" $ | nindent 4 }} 39 | {{- end }} 40 | {{- end }} 41 | -------------------------------------------------------------------------------- /chart/templates/query-deploy.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.query.enabled -}} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ include "jaeger-ydb-store.fullname" . }}-query 6 | labels: 7 | {{- include "jaeger-ydb-store.labels" . | nindent 4 }} 8 | {{- include "jaeger-ydb-store.querySelectorLabels" $ | nindent 4 }} 9 | spec: 10 | replicas: {{ .Values.query.replicas }} 11 | selector: 12 | matchLabels: 13 | {{- include "jaeger-ydb-store.selectorLabels" . | nindent 6 }} 14 | {{- include "jaeger-ydb-store.querySelectorLabels" $ | nindent 6 }} 15 | template: 16 | metadata: 17 | annotations: 18 | {{- if .Values.ydb.saPrivateKey }} 19 | checksum/secrets: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} 20 | {{- end }} 21 | {{- with .Values.query.podAnnotations }} 22 | {{- toYaml . | nindent 8 }} 23 | {{- end }} 24 | labels: 25 | {{- include "jaeger-ydb-store.selectorLabels" . | nindent 8 }} 26 | {{- include "jaeger-ydb-store.querySelectorLabels" . | nindent 8 }} 27 | spec: 28 | {{- with .Values.imagePullSecrets }} 29 | imagePullSecrets: 30 | {{- toYaml . | nindent 8 }} 31 | {{- end }} 32 | serviceAccountName: {{ include "jaeger-ydb-store.serviceAccountName" . }} 33 | securityContext: 34 | {{- toYaml .Values.query.podSecurityContext | nindent 8 }} 35 | containers: 36 | - name: query 37 | env: 38 | {{- include "jaeger-ydb-store.ydb.env" . | nindent 10 }} 39 | securityContext: 40 | {{- toYaml .Values.query.securityContext | nindent 12 }} 41 | image: "{{ .Values.image.query.repository }}:{{ .Values.image.query.tag }}" 42 | imagePullPolicy: {{ .Values.image.query.pullPolicy }} 43 | livenessProbe: 44 | httpGet: 45 | path: / 46 | port: health 47 | ports: 48 | - name: health 49 | containerPort: 16687 50 | protocol: TCP 51 | {{- range $key, $value := merge dict .Values.service.query.ports }} 52 | - name: {{ $key }} 53 | containerPort: {{ $value.port }} 54 | protocol: {{ default "TCP" $value.protocol }} 55 | {{- end }} 56 | {{- if .Values.ydb.saPrivateKey }} 57 | volumeMounts: 58 | - mountPath: /opt/secrets 59 | name: secrets 60 | readOnly: true 61 | {{- end }} 62 | resources: 63 | {{- toYaml .Values.resources.query | nindent 12 }} 64 | {{- if .Values.ydb.saPrivateKey }} 65 | volumes: 66 | - name: secrets 67 | secret: 68 | secretName: {{ include "jaeger-ydb-store.fullname" . }} 69 | {{- end }} 70 | {{- with .Values.query.nodeSelector }} 71 | nodeSelector: 72 | {{- toYaml . | nindent 8 }} 73 | {{- end }} 74 | {{- with .Values.query.affinity }} 75 | affinity: 76 | {{- toYaml . | nindent 8 }} 77 | {{- end }} 78 | {{- with .Values.query.tolerations }} 79 | tolerations: 80 | {{- toYaml . | nindent 8 }} 81 | {{- end }} 82 | {{- end -}} 83 | -------------------------------------------------------------------------------- /chart/templates/query-svc.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.query.enabled -}} 2 | {{- with .Values.service.query }} 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: {{ include "jaeger-ydb-store.fullname" $ }}-query 7 | {{- with .annotations }} 8 | annotations: 9 | {{- toYaml . | nindent 4 }} 10 | {{- end }} 11 | labels: 12 | {{- include "jaeger-ydb-store.labels" $ | nindent 4 }} 13 | {{- include "jaeger-ydb-store.querySelectorLabels" $ | nindent 4 }} 14 | spec: 15 | type: {{ .type }} 16 | {{- if .headless }} 17 | clusterIP: None 18 | {{- end }} 19 | {{- with .ipFamilies }} 20 | ipFamilies: 21 | {{- toYaml . | nindent 4 }} 22 | {{- end }} 23 | {{- if .ipFamilyPolicy }} 24 | ipFamilyPolicy: {{ .ipFamilyPolicy }} 25 | {{- end }} 26 | ports: 27 | {{- range $key, $value := .ports }} 28 | - name: {{ $value.name }} 29 | port: {{ $value.port }} 30 | targetPort: {{ $value.name }} 31 | protocol: {{ default "TCP" $value.protocol }} 32 | {{- if $value.nodePort }} 33 | nodePort: {{ $value.nodePort }} 34 | {{- end }} 35 | {{- end }} 36 | selector: 37 | {{- include "jaeger-ydb-store.selectorLabels" $ | nindent 4 }} 38 | {{- include "jaeger-ydb-store.querySelectorLabels" $ | nindent 4 }} 39 | {{- end }} 40 | {{- end }} 41 | -------------------------------------------------------------------------------- /chart/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ydb.saPrivateKey }} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: {{ include "jaeger-ydb-store.fullname" . }} 6 | labels: 7 | {{- include "jaeger-ydb-store.labels" . | nindent 4 }} 8 | data: 9 | ydb-sa-key.pem: {{ .Values.ydb.saPrivateKey | b64enc }} 10 | {{- end }} 11 | -------------------------------------------------------------------------------- /chart/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "jaeger-ydb-store.serviceAccountName" . }} 6 | labels: 7 | {{- include "jaeger-ydb-store.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /chart/templates/watcher-deploy.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.watcher.enabled -}} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ include "jaeger-ydb-store.fullname" . }}-watcher 6 | labels: 7 | {{- include "jaeger-ydb-store.labels" . | nindent 4 }} 8 | {{- include "jaeger-ydb-store.watcherSelectorLabels" . | nindent 4 }} 9 | spec: 10 | replicas: {{ .Values.watcher.replicas }} 11 | selector: 12 | matchLabels: 13 | {{- include "jaeger-ydb-store.selectorLabels" . | nindent 6 }} 14 | {{- include "jaeger-ydb-store.watcherSelectorLabels" . | nindent 6 }} 15 | template: 16 | metadata: 17 | annotations: 18 | {{- if .Values.ydb.saPrivateKey }} 19 | checksum/secrets: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} 20 | {{- end }} 21 | {{- with .Values.watcher.podAnnotations }} 22 | {{- toYaml . | nindent 8 }} 23 | {{- end }} 24 | labels: 25 | {{- include "jaeger-ydb-store.selectorLabels" . | nindent 8 }} 26 | {{- include "jaeger-ydb-store.watcherSelectorLabels" $ | nindent 8 }} 27 | spec: 28 | {{- with .Values.imagePullSecrets }} 29 | imagePullSecrets: 30 | {{- toYaml . | nindent 8 }} 31 | {{- end }} 32 | serviceAccountName: {{ include "jaeger-ydb-store.serviceAccountName" . }} 33 | securityContext: 34 | {{- toYaml .Values.watcher.podSecurityContext | nindent 8 }} 35 | containers: 36 | - name: watcher 37 | args: 38 | - watcher 39 | env: 40 | {{- include "jaeger-ydb-store.ydb.env" . | nindent 10 }} 41 | securityContext: 42 | {{- toYaml .Values.watcher.securityContext | nindent 12 }} 43 | image: "{{ .Values.image.watcher.repository }}:{{ .Values.image.watcher.tag }}" 44 | imagePullPolicy: {{ .Values.image.watcher.pullPolicy }} 45 | {{- if .Values.ydb.saPrivateKey }} 46 | volumeMounts: 47 | - mountPath: /opt/secrets 48 | name: secrets 49 | readOnly: true 50 | {{- end }} 51 | resources: 52 | {{- toYaml .Values.resources.watcher | nindent 12 }} 53 | {{- if .Values.ydb.saPrivateKey }} 54 | volumes: 55 | - name: secrets 56 | secret: 57 | secretName: {{ include "jaeger-ydb-store.fullname" . }} 58 | {{- end }} 59 | {{- with .Values.watcher.nodeSelector }} 60 | nodeSelector: 61 | {{- toYaml . | nindent 8 }} 62 | {{- end }} 63 | {{- with .Values.watcher.affinity }} 64 | affinity: 65 | {{- toYaml . | nindent 8 }} 66 | {{- end }} 67 | {{- with .Values.watcher.tolerations }} 68 | tolerations: 69 | {{- toYaml . | nindent 8 }} 70 | {{- end }} 71 | {{- end -}} 72 | -------------------------------------------------------------------------------- /chart/templates/ydb-environment.tpl: -------------------------------------------------------------------------------- 1 | {{- define "jaeger-ydb-store.ydb.env" -}} 2 | {{- range $key, $value :=.Values.ydb.env }} 3 | - name: {{ $key }} 4 | value: {{ $value | quote }} 5 | {{- end }} 6 | {{- if .Values.ydb.endpoint }} 7 | - name: YDB_ADDRESS 8 | value: {{ .Values.ydb.endpoint | quote }} 9 | {{- end }} 10 | {{- if .Values.ydb.database }} 11 | - name: YDB_PATH 12 | value: {{ .Values.ydb.database | quote }} 13 | {{- end }} 14 | {{- if .Values.ydb.anonymous }} 15 | - name: YDB_ANONYMOUS 16 | value: {{ .Values.ydb.anonymous | quote }} 17 | {{- end }} 18 | {{- if .Values.ydb.token }} 19 | - name: YDB_TOKEN 20 | value: {{ .Values.ydb.token | quote }} 21 | {{- end }} 22 | {{- if .Values.ydb.useMetaAuth }} 23 | - name: YDB_SA_META_AUTH 24 | value: {{ .Values.ydb.useMetaAuth | quote }} 25 | {{- end }} 26 | {{- if .Values.ydb.saKeyJson }} 27 | - name: YDB_SA_KEY_JSON 28 | value: {{ .Values.ydb.saKeyJson | quote }} 29 | {{- end }} 30 | {{- if .Values.ydb.saId }} 31 | - name: YDB_SA_ID 32 | value: {{ .Values.ydb.saId | quote }} 33 | {{- end }} 34 | {{- if .Values.ydb.saKeyId }} 35 | - name: YDB_SA_KEY_ID 36 | value: {{ .Values.ydb.saKeyId | quote }} 37 | {{- end }} 38 | {{- if .Values.ydb.saPrivateKey }} 39 | - name: YDB_SA_PRIVATE_KEY_FILE 40 | value: /opt/secrets/ydb-sa-key.pem 41 | {{- end }} 42 | {{- if .Values.ydb.secureConnection }} 43 | - name: YDB_SECURE_CONNECTION 44 | value: {{ .Values.ydb.secureConnection | quote }} 45 | {{- end }} 46 | - name: YDB_FOLDER 47 | value: {{ default "jaeger" .Values.ydb.folder | quote }} 48 | {{- end -}} 49 | -------------------------------------------------------------------------------- /chart/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for jaeger-ydb-store. 2 | 3 | ydb: 4 | # DB endpoint host:port to connect to 5 | endpoint: 6 | 7 | # Database path 8 | database: 9 | 10 | # Folder to store data in 11 | folder: 12 | 13 | # one of ["enabled", "disabled"] 14 | secureConnection: "" 15 | 16 | # true if anonymous authorization 17 | anonymous: "" 18 | 19 | # access token for authorization 20 | token: "" 21 | 22 | # Service account key in json format for Yandex.Cloud authorization 23 | saKeyJson: "" 24 | 25 | # Service account id for Yandex.Cloud authorization 26 | # Deprecated: now part of keyYdbSaKeyJson 27 | saId: "" 28 | 29 | # Service account key id for Yandex.Cloud authorization 30 | # Deprecated: now part of keyYdbSaKeyJson 31 | saKeyId: "" 32 | 33 | # Service account private key for Yandex.Cloud authorization 34 | # Deprecated: now part of keyYdbSaKeyJson 35 | saPrivateKey: "" 36 | 37 | # Use metadata to authorize requests 38 | useMetaAuth: true 39 | 40 | env: 41 | # DB connect timeout 42 | # YDB_CONNECT_TIMEOUT: 10s 43 | 44 | # Write queries timeout 45 | # YDB_WRITE_TIMEOUT: 10s 46 | 47 | # attempt to write queries timeout 48 | # YDB_RETRY_ATTEMPT_TIMEOUT: 1s 49 | 50 | # Read queries timeout 51 | # YDB_READ_TIMEOUT: 10s 52 | 53 | # Controls number of parallel read subqueries 54 | # YDB_READ_QUERY_PARALLEL: 16 55 | 56 | # Max operation names to fetch for service 57 | # YDB_READ_OP_LIMIT: 5000 58 | 59 | # Max service names to fetch 60 | # YDB_READ_SVC_LIMIT: 1000 61 | 62 | # DB session pool size 63 | # YDB_POOL_SIZE: 100 64 | 65 | # DB query cache size 66 | # YDB_QUERY_CACHE_SIZE: 50 67 | 68 | # Span buffer size for batch writer 69 | # YDB_WRITER_BUFFER_SIZE: 1000 70 | 71 | # Number of spans in batch write calls 72 | # YDB_WRITER_BATCH_SIZE: 100 73 | 74 | # Number of workers processing batch writes 75 | # YDB_WRITER_BATCH_WORKERS: 10 76 | 77 | # Span buffer size for indexer 78 | # YDB_INDEXER_BUFFER_SIZE: 1000 79 | 80 | # Maximum trace_id count in a sinigle index record 81 | # YDB_INDEXER_MAX_TRACES: 100 82 | 83 | # Maximum amount of time for indexer to batch trace_idы for index records 84 | # YDB_INDEXER_MAX_TTL: 5s 85 | 86 | # Number of partitioned tables per day. Changing it requires recreating full data set 87 | # YDB_SCHEMA_NUM_PARTITIONS: 10 88 | 89 | # PLUGIN_LOG_PATH: /tmp/plugin.log 90 | 91 | # Delete partition tables older than this value 92 | # WATCHER_AGE: 24h 93 | 94 | # Check interval 95 | # WATCHER_INTERVAL: 5m 96 | 97 | # Enable table split by load feature 98 | # YDB_FEATURE_SPLIT_BY_LOAD: false 99 | 100 | # Enable table compression feature, used for span storage 101 | # YDB_FEATURE_COMPRESSION: false 102 | 103 | 104 | image: 105 | query: 106 | pullPolicy: IfNotPresent 107 | repository: cr.yandex/yc/jaeger-ydb-query 108 | tag: v1.9.1 109 | collector: 110 | pullPolicy: IfNotPresent 111 | repository: cr.yandex/yc/jaeger-ydb-collector 112 | tag: v1.9.1 113 | watcher: 114 | pullPolicy: IfNotPresent 115 | repository: cr.yandex/yc/jaeger-ydb-watcher 116 | tag: v1.9.1 117 | agent: 118 | pullPolicy: IfNotPresent 119 | repository: jaegertracing/jaeger-agent 120 | tag: 1.44 121 | 122 | imagePullSecrets: [] 123 | nameOverride: "" 124 | fullnameOverride: "" 125 | 126 | collector: 127 | enabled: true 128 | replicas: 3 129 | affinity: {} 130 | nodeSelector: {} 131 | tolerations: [] 132 | securityContext: {} 133 | podSecurityContext: {} 134 | podAnnotations: {} 135 | 136 | query: 137 | enabled: true 138 | replicas: 3 139 | affinity: {} 140 | nodeSelector: {} 141 | tolerations: [] 142 | securityContext: {} 143 | podSecurityContext: {} 144 | podAnnotations: {} 145 | 146 | watcher: 147 | enabled: true 148 | replicas: 2 149 | affinity: {} 150 | nodeSelector: {} 151 | tolerations: [] 152 | securityContext: {} 153 | podSecurityContext: {} 154 | podAnnotations: {} 155 | 156 | agent: 157 | enabled: true 158 | podSecurityContext: {} 159 | securityContext: {} 160 | annotations: {} 161 | pullPolicy: IfNotPresent 162 | cmdlineParams: {} 163 | extraEnv: [] 164 | daemonset: 165 | useHostPort: false 166 | updateStrategy: {} 167 | # type: RollingUpdate 168 | # rollingUpdate: 169 | # maxUnavailable: 1 170 | nodeSelector: {} 171 | tolerations: [] 172 | affinity: {} 173 | podAnnotations: {} 174 | extraSecretMounts: [] 175 | # - name: jaeger-tls 176 | # mountPath: /tls 177 | # subPath: "" 178 | # secretName: jaeger-tls 179 | # readOnly: true 180 | extraConfigmapMounts: [] 181 | # - name: jaeger-config 182 | # mountPath: /config 183 | # subPath: "" 184 | # configMap: jaeger-config 185 | # readOnly: true 186 | useHostNetwork: false 187 | dnsPolicy: ClusterFirst 188 | priorityClassName: "" 189 | 190 | 191 | service: 192 | collector: 193 | type: ClusterIP 194 | # headless: false 195 | # ipFamilies: 196 | # - IPv6 197 | # - IPv4 198 | # ipFamilyPolicy: PreferDualStack 199 | annotations: {} 200 | ports: 201 | grpc: 202 | name: grpc 203 | port: 14250 204 | # nodePort: 34250 205 | http: 206 | name: http 207 | port: 14268 208 | query: 209 | type: LoadBalancer 210 | annotations: {} 211 | ports: 212 | http: 213 | name: http 214 | port: 16686 215 | agent: 216 | type: ClusterIP 217 | annotations: {} 218 | ports: 219 | zipkin-compact: 220 | # Accept zipkin.thrift over compact thrift protocol 221 | name: zipkin-compact 222 | port: 5775 223 | protocol: UDP 224 | jaeger-compact: 225 | # Accept jaeger.thrift over compact thrift protocol 226 | name: jaeger-compact 227 | port: 6831 228 | protocol: UDP 229 | jaeger-binary: 230 | # Accept jaeger.thrift over binary thrift protocol 231 | name: jaeger-binary 232 | port: 6832 233 | protocol: UDP 234 | http: 235 | # (HTTP) serve configs, sampling strategies 236 | name: http 237 | port: 5778 238 | protocol: TCP 239 | 240 | resources: 241 | watcher: 242 | limits: {} 243 | requests: {} 244 | collector: 245 | limits: {} 246 | requests: {} 247 | query: 248 | limits: {} 249 | requests: {} 250 | agent: 251 | limits: {} 252 | requests: {} 253 | 254 | 255 | serviceAccount: 256 | # Specifies whether a service account should be created 257 | create: true 258 | # Annotations to add to the service account 259 | annotations: {} 260 | # The name of the service account to use. 261 | # If not set and create is true, a name is generated using the fullname template 262 | name: "" 263 | -------------------------------------------------------------------------------- /cmd/schema/cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "strings" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/spf13/cobra" 14 | "github.com/spf13/viper" 15 | "github.com/ydb-platform/ydb-go-sdk/v3" 16 | "github.com/ydb-platform/ydb-go-sdk/v3/scheme" 17 | "github.com/ydb-platform/ydb-go-sdk/v3/sugar" 18 | "github.com/ydb-platform/ydb-go-sdk/v3/table" 19 | "go.uber.org/zap" 20 | 21 | "github.com/ydb-platform/jaeger-ydb-store/cmd/schema/watcher" 22 | "github.com/ydb-platform/jaeger-ydb-store/internal/db" 23 | localViper "github.com/ydb-platform/jaeger-ydb-store/internal/viper" 24 | "github.com/ydb-platform/jaeger-ydb-store/schema" 25 | ) 26 | 27 | func init() { 28 | viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) 29 | viper.SetDefault("watcher_interval", time.Minute*5) 30 | viper.SetDefault("watcher_age", time.Hour*24) 31 | viper.SetDefault("watcher_lookahead", time.Hour*12) 32 | viper.SetDefault("parts_traces", 32) 33 | viper.SetDefault("parts_idx_tag", 32) 34 | viper.SetDefault("parts_idx_tag_v2", 32) 35 | viper.SetDefault("parts_idx_duration", 32) 36 | viper.SetDefault("parts_idx_service_name", 32) 37 | viper.SetDefault("parts_idx_service_op", 32) 38 | viper.SetDefault(db.KeyYDBPartitionSize, "1024mb") 39 | viper.AutomaticEnv() 40 | } 41 | 42 | func main() { 43 | localViper.ConfigureViperFromFlag(viper.GetViper()) 44 | 45 | command := &cobra.Command{ 46 | Use: "jaeger-ydb-schema", 47 | } 48 | command.PersistentFlags().String("address", "", "ydb host:port (env: YDB_ADDRESS)") 49 | command.PersistentFlags().String("path", "", "ydb path (env: YDB_PATH)") 50 | command.PersistentFlags().String("folder", "", "ydb folder (env: YDB_FOLDER)") 51 | command.PersistentFlags().String("token", "", "ydb oauth token (env: YDB_TOKEN)") 52 | command.PersistentFlags().String("config", "", "path to config file to configure Viper from") 53 | 54 | _ = viper.BindPFlag("ydb_address", command.PersistentFlags().Lookup("address")) 55 | _ = viper.BindPFlag("ydb_path", command.PersistentFlags().Lookup("path")) 56 | _ = viper.BindPFlag("ydb_folder", command.PersistentFlags().Lookup("folder")) 57 | _ = viper.BindPFlag("ydb_token", command.PersistentFlags().Lookup("token")) 58 | 59 | cfg := zap.NewProductionConfig() 60 | logger, err := cfg.Build() 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | 65 | watcherCmd := &cobra.Command{ 66 | Use: "watcher", 67 | RunE: func(cmd *cobra.Command, args []string) error { 68 | ctx, cancel := context.WithCancel(context.Background()) 69 | defer cancel() 70 | opts := watcher.Options{ 71 | Expiration: viper.GetDuration("watcher_age"), 72 | Lookahead: viper.GetDuration("watcher_lookahead"), 73 | DBPath: schema.DbPath{ 74 | Path: viper.GetString(db.KeyYdbPath), 75 | Folder: viper.GetString(db.KeyYdbFolder), 76 | }, 77 | } 78 | if opts.Expiration == 0 { 79 | return fmt.Errorf("cannot use watcher age '%s'", opts.Expiration) 80 | } 81 | 82 | shutdown := make(chan os.Signal, 1) 83 | signal.Notify(shutdown, syscall.SIGINT, syscall.SIGTERM) 84 | conn, err := ydbConn(ctx, viper.GetViper(), nil) 85 | if err != nil { 86 | return fmt.Errorf("failed to create table client: %w", err) 87 | } 88 | 89 | logger.Info("starting watcher") 90 | w := watcher.NewWatcher(opts, conn.Table(), logger) 91 | w.Run(viper.GetDuration("watcher_interval")) 92 | <-shutdown 93 | logger.Info("stopping watcher") 94 | return nil 95 | }, 96 | } 97 | dropCmd := &cobra.Command{ 98 | Use: "drop-tables", 99 | RunE: func(cmd *cobra.Command, args []string) error { 100 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 101 | defer cancel() 102 | 103 | dbPath := schema.DbPath{ 104 | Path: viper.GetString(db.KeyYdbPath), 105 | Folder: viper.GetString(db.KeyYdbFolder), 106 | } 107 | conn, err := ydbConn(ctx, viper.GetViper(), logger) 108 | if err != nil { 109 | return fmt.Errorf("failed to create table client: %w", err) 110 | } 111 | d, err := conn.Scheme().ListDirectory(ctx, dbPath.String()) 112 | if err != nil { 113 | return err 114 | } 115 | for _, c := range d.Children { 116 | if c.Type == scheme.EntryTable { 117 | opCtx, opCancel := context.WithTimeout(context.Background(), time.Second*5) 118 | fullName := dbPath.FullTable(c.Name) 119 | fmt.Printf("dropping table '%s'\n", fullName) 120 | err = conn.Table().Do(opCtx, func(ctx context.Context, session table.Session) error { 121 | return session.DropTable(ctx, fullName) 122 | }) 123 | opCancel() 124 | if err != nil { 125 | return err 126 | } 127 | } 128 | } 129 | return nil 130 | }, 131 | } 132 | command.AddCommand(watcherCmd, dropCmd) 133 | 134 | err = command.Execute() 135 | if err != nil { 136 | log.Fatal(err) 137 | } 138 | } 139 | 140 | func ydbConn(ctx context.Context, v *viper.Viper, l *zap.Logger) (*ydb.Driver, error) { 141 | ctx, cancel := context.WithTimeout(ctx, time.Second*5) 142 | defer cancel() 143 | 144 | isSecure, err := db.GetIsSecureWithDefault(v, false) 145 | if err != nil { 146 | return nil, err 147 | } 148 | return db.DialFromViper(ctx, v, l, sugar.DSN(v.GetString(db.KeyYdbAddress), v.GetString(db.KeyYdbPath), isSecure)) 149 | } 150 | -------------------------------------------------------------------------------- /cmd/schema/watcher/lru.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | lru "github.com/hashicorp/golang-lru" 5 | ) 6 | 7 | func mustNewLRU(size int) *lru.Cache { 8 | cache, err := lru.New(size) 9 | if err != nil { 10 | panic(err) 11 | } 12 | return cache 13 | } 14 | -------------------------------------------------------------------------------- /cmd/schema/watcher/tables.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | 6 | "github.com/ydb-platform/jaeger-ydb-store/schema" 7 | ) 8 | 9 | type partDefinition struct { 10 | defFunc schema.PartitionedDefinition 11 | count uint64 12 | } 13 | 14 | func definitions() map[string]partDefinition { 15 | m := make(map[string]partDefinition, len(schema.PartitionTables)) 16 | for name, f := range schema.PartitionTables { 17 | m[name] = partDefinition{f, viper.GetUint64("parts_" + name)} 18 | } 19 | return m 20 | } 21 | -------------------------------------------------------------------------------- /cmd/schema/watcher/watcher.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | lru "github.com/hashicorp/golang-lru" 9 | ydb "github.com/ydb-platform/ydb-go-sdk/v3" 10 | "github.com/ydb-platform/ydb-go-sdk/v3/table" 11 | "go.uber.org/zap" 12 | 13 | "github.com/ydb-platform/jaeger-ydb-store/internal/db" 14 | "github.com/ydb-platform/jaeger-ydb-store/schema" 15 | ) 16 | 17 | const ( 18 | operationTimeout = time.Minute 19 | ) 20 | 21 | var txc = table.DefaultTxControl() 22 | 23 | type Options struct { 24 | Expiration time.Duration 25 | Lookahead time.Duration 26 | DBPath schema.DbPath 27 | } 28 | 29 | type Watcher struct { 30 | sessionProvider table.Client 31 | opts Options 32 | logger *zap.Logger 33 | 34 | ticker *time.Ticker 35 | tableDefinitions map[string]partDefinition 36 | knownTables *lru.Cache 37 | } 38 | 39 | func NewWatcher(opts Options, sp table.Client, logger *zap.Logger) *Watcher { 40 | return &Watcher{ 41 | sessionProvider: sp, 42 | opts: opts, 43 | logger: logger, 44 | 45 | tableDefinitions: definitions(), 46 | knownTables: mustNewLRU(500), 47 | } 48 | } 49 | 50 | func (w *Watcher) Run(interval time.Duration) { 51 | w.ticker = time.NewTicker(interval) 52 | go func() { 53 | w.once() 54 | for range w.ticker.C { 55 | w.once() 56 | } 57 | }() 58 | } 59 | 60 | func (w *Watcher) once() { 61 | err := w.createTables() 62 | if err != nil { 63 | w.logger.Error("create tables failed", 64 | zap.Error(err), 65 | ) 66 | return 67 | } 68 | w.dropOldTables() 69 | } 70 | 71 | func (w *Watcher) createTables() error { 72 | t := time.Now() 73 | ctx, cancel := context.WithTimeout(context.Background(), operationTimeout) 74 | defer cancel() 75 | 76 | for name, definition := range schema.Tables { 77 | fullName := w.opts.DBPath.FullTable(name) 78 | if w.tableKnown(ctx, fullName) { 79 | // We already created this table, skip 80 | continue 81 | } 82 | err := w.sessionProvider.Do(ctx, func(ctx context.Context, session table.Session) error { 83 | return session.CreateTable(ctx, fullName, definition()...) 84 | }) 85 | if err != nil { 86 | w.logger.Error("create table failed", 87 | zap.String("name", fullName), zap.Error(err), 88 | ) 89 | return err 90 | } 91 | // save knowledge about table for later 92 | w.knownTables.Add(fullName, struct{}{}) 93 | } 94 | parts := schema.MakePartitionList(t, t.Add(w.opts.Lookahead)) 95 | for _, part := range parts { 96 | w.logger.Info("creating partition", zap.String("suffix", part.Suffix())) 97 | if err := w.createTablesForPartition(ctx, part); err != nil { 98 | return err 99 | } 100 | err := w.sessionProvider.Do(ctx, func(ctx context.Context, session table.Session) error { 101 | _, _, err := session.Execute(ctx, txc, schema.BuildQuery(w.opts.DBPath, schema.InsertPart), part.QueryParams()) 102 | return err 103 | }) 104 | if err != nil { 105 | w.logger.Error("partition save failed", 106 | zap.String("suffix", part.Suffix()), zap.Error(err), 107 | ) 108 | return err 109 | } 110 | } 111 | return nil 112 | } 113 | 114 | func (w *Watcher) createTablesForPartition(ctx context.Context, part schema.PartitionKey) error { 115 | for name, def := range w.tableDefinitions { 116 | fullName := part.BuildFullTableName(w.opts.DBPath.String(), name) 117 | if w.tableKnown(ctx, fullName) { 118 | // We already created this table, skip 119 | continue 120 | } 121 | err := w.sessionProvider.Do(ctx, func(ctx context.Context, session table.Session) error { 122 | return session.CreateTable(ctx, fullName, def.defFunc(def.count)...) 123 | }) 124 | if err != nil { 125 | w.logger.Error("create table failed", 126 | zap.String("name", fullName), zap.Error(err), 127 | ) 128 | return err 129 | } 130 | // save knowledge about table for later 131 | w.knownTables.Add(fullName, struct{}{}) 132 | } 133 | return nil 134 | } 135 | 136 | func (w *Watcher) dropOldTables() { 137 | expireTime := time.Now().Add(-w.opts.Expiration) 138 | w.logger.Info("delete old tables", zap.Time("before", expireTime)) 139 | ctx, cancel := context.WithTimeout(context.Background(), operationTimeout) 140 | defer cancel() 141 | 142 | query := schema.BuildQuery(w.opts.DBPath, schema.QueryParts) 143 | _ = w.sessionProvider.Do(ctx, func(ctx context.Context, session table.Session) error { 144 | _, res, err := session.Execute(ctx, txc, query, nil) 145 | if err != nil { 146 | w.logger.Error("partition list query failed", zap.Error(err)) 147 | return err 148 | } 149 | for res.NextResultSet(ctx) { 150 | for res.NextRow() { 151 | part := schema.PartitionKey{} 152 | err = res.ScanWithDefaults(&part.Date, &part.Num, &part.IsActive) 153 | if err != nil { 154 | w.logger.Error("partition scan failed", zap.Error(err)) 155 | return fmt.Errorf("part scan err: %w", err) 156 | } 157 | _, t := part.TimeSpan() 158 | if expireTime.Sub(t) > 0 { 159 | if part.IsActive { 160 | err := w.markPartitionForDelete(ctx, session, part) 161 | if err != nil { 162 | w.logger.Error("update partition failed", zap.String("suffix", part.Suffix()), zap.Error(err)) 163 | } 164 | } else { 165 | w.logger.Info("delete partition", zap.String("suffix", part.Suffix())) 166 | if err := w.dropTables(ctx, session, part); err != nil { 167 | continue 168 | } 169 | err = w.deletePartitionInfo(ctx, session, part) 170 | if err != nil { 171 | w.logger.Error("delete partition failed", zap.String("suffix", part.Suffix()), zap.Error(err)) 172 | } 173 | } 174 | } 175 | } 176 | } 177 | return res.Err() 178 | }) 179 | } 180 | 181 | func (w *Watcher) dropTables(ctx context.Context, session table.Session, k schema.PartitionKey) error { 182 | for name := range schema.PartitionTables { 183 | fullName := k.BuildFullTableName(w.opts.DBPath.String(), name) 184 | err := session.DropTable(ctx, fullName) 185 | if err != nil { 186 | opErr := ydb.OperationError(err) 187 | switch { 188 | // table or path already removed, ignore err 189 | case opErr != nil && db.IssueContainsMessage(err, "EPathStateNotExist"): 190 | case ydb.IsOperationErrorSchemeError(err) && db.IssueContainsMessage(err, "Path does not exist"): 191 | default: 192 | w.logger.Error("drop table failed", zap.String("table", fullName), zap.Error(err)) 193 | return err 194 | } 195 | } 196 | } 197 | return nil 198 | } 199 | 200 | func (w *Watcher) markPartitionForDelete(ctx context.Context, session table.Session, k schema.PartitionKey) error { 201 | k.IsActive = false 202 | _, _, err := session.Execute(ctx, txc, schema.BuildQuery(w.opts.DBPath, schema.UpdatePart), k.QueryParams()) 203 | if err != nil { 204 | return err 205 | } 206 | return nil 207 | } 208 | 209 | func (w *Watcher) deletePartitionInfo(ctx context.Context, session table.Session, k schema.PartitionKey) error { 210 | _, _, err := session.Execute(ctx, txc, schema.BuildQuery(w.opts.DBPath, schema.DeletePart), k.QueryWhereParams()) 211 | if err != nil { 212 | return err 213 | } 214 | return nil 215 | } 216 | 217 | func (w *Watcher) tableKnown(ctx context.Context, fullName string) bool { 218 | if _, ok := w.knownTables.Get(fullName); ok { 219 | return true 220 | } 221 | err := w.sessionProvider.Do(ctx, func(ctx context.Context, session table.Session) error { 222 | _, err := session.DescribeTable(ctx, fullName) 223 | return err 224 | }) 225 | if err != nil { 226 | return false 227 | } 228 | w.knownTables.Add(fullName, struct{}{}) 229 | return true 230 | } 231 | -------------------------------------------------------------------------------- /docker-compose.example.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | schema: 4 | image: cr.yandex/yc/jaeger-ydb-watcher 5 | environment: 6 | YDB_ADDRESS: lb.etn020jbq151nk3l676n.ydb.mdb.yandexcloud.net:2135 7 | YDB_PATH: /ru-central1/b1gr01sh5qjihf7elkiv/etn020jbq151nk3l676n 8 | YDB_SA_KEY_JSON: > 9 | { 10 | "id": "ajes5qdgf6osjamjecd7", 11 | "service_account_id": "aje65tbjpm9c2l7kecnl", 12 | "private_key": "-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----\n" 13 | } 14 | YDB_SECURE_CONNECTION: enabled 15 | command: watcher 16 | collector: 17 | image: cr.yandex/yc/jaeger-ydb-collector 18 | environment: 19 | YDB_ADDRESS: lb.etn020jbq151nk3l676n.ydb.mdb.yandexcloud.net:2135 20 | YDB_PATH: /ru-central1/b1gr01sh5qjihf7elkiv/etn020jbq151nk3l676n 21 | PLUGIN_LOG_PATH: /tmp/plugin.log 22 | YDB_SA_KEY_JSON: > 23 | { 24 | "id": "ajes5qdgf6osjamjecd7", 25 | "service_account_id": "aje65tbjpm9c2l7kecnl", 26 | "private_key": "-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----\n" 27 | } 28 | YDB_SECURE_CONNECTION: enabled 29 | query: 30 | image: cr.yandex/yc/jaeger-ydb-query 31 | ports: 32 | - 16686:16686 33 | environment: 34 | YDB_ADDRESS: lb.etn020jbq151nk3l676n.ydb.mdb.yandexcloud.net:2135 35 | YDB_PATH: /ru-central1/b1gr01sh5qjihf7elkiv/etn020jbq151nk3l676n 36 | YDB_SA_KEY_JSON: > 37 | { 38 | "id": "ajes5qdgf6osjamjecd7", 39 | "service_account_id": "aje65tbjpm9c2l7kecnl", 40 | "private_key": "-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----\n" 41 | } 42 | YDB_SECURE_CONNECTION: enabled 43 | PLUGIN_LOG_PATH: /tmp/plugin.log -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | 1. Select which YDB managed service flavour you will be using (dedicated or serverless). 4 | 2. Edit proper docker-compose configuration, specifying full database path and endpoint to connect to. 5 | 3. Create service account with either `editor` or `ydb.admin` role assigned. 6 | 4. Deploy VM with the SA previously created attached to this VM. 7 | Also, you will need to verify proper network conditions: 8 | * Dedicated: either deploy in the same network 9 | (if YDB managed service isn't configured to provide external IP address) 10 | or with Internet access. 11 | * Serverless: deploy onto VM with Internet access. 12 | -------------------------------------------------------------------------------- /examples/docker-compose.dedicated.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | schema: 4 | image: cr.yandex/yc/jaeger-ydb-watcher:dev 5 | environment: 6 | PARTS_IDX_TAG_V2: 4 7 | PARTS_TRACES: 4 8 | PARTS_IDX_SERVICE_OP: 4 9 | PARTS_DURATION: 4 10 | PARTS_IDX_SERVICE_NAME: 4 11 | YDB_SA_META_AUTH: "true" 12 | YDB_CA_FILE: "/ydb-ca.pem" 13 | YDB_ADDRESS: lb.zzz.ydb.mdb.yandexcloud.net:2135 14 | YDB_PATH: /xxx/yyy/zzz 15 | YDB_FEATURE_COMPRESSION: "true" 16 | YDB_FEATURE_SPLIT_BY_LOAD: "true" 17 | command: watcher 18 | collector: 19 | image: cr.yandex/yc/jaeger-ydb-collector:dev 20 | ports: 21 | - 14269:14269 22 | environment: 23 | YDB_SA_META_AUTH: "true" 24 | YDB_CA_FILE: "/ydb-ca.pem" 25 | YDB_ADDRESS: lb.zzz.ydb.mdb.yandexcloud.net:2135 26 | YDB_PATH: /xxx/yyy/zzz 27 | YDB_FEATURE_COMPRESSION: "true" 28 | YDB_FEATURE_SPLIT_BY_LOAD: "true" 29 | YDB_WRITER_BUFFER_SIZE: 50000 30 | YDB_WRITER_BATCH_SIZE: 5000 31 | YDB_WRITER_BATCH_WORKERS: 100 32 | YDB_INDEXER_BUFFER_SIZE: 5000 33 | COLLECTOR_QUEUE_SIZE: 50000 34 | PLUGIN_LOG_PATH: /tmp/plugin.log 35 | query: 36 | image: cr.yandex/yc/jaeger-ydb-query:dev 37 | ports: 38 | - 16686:16686 39 | environment: 40 | YDB_SA_META_AUTH: "true" 41 | YDB_CA_FILE: "/ydb-ca.pem" 42 | YDB_ADDRESS: lb.zzz.ydb.mdb.yandexcloud.net:2135 43 | YDB_PATH: /xxx/yyy/zzz 44 | YDB_FEATURE_COMPRESSION: "true" 45 | YDB_FEATURE_SPLIT_BY_LOAD: "true" 46 | PLUGIN_LOG_PATH: /tmp/plugin.log 47 | agent: 48 | image: jaegertracing/jaeger-agent 49 | command: 50 | - "--reporter.grpc.host-port=collector:14250" 51 | - "--processor.jaeger-binary.server-queue-size=10000" 52 | - "--processor.jaeger-compact.server-queue-size=10000" 53 | - "--processor.zipkin-compact.server-queue-size=10000" 54 | ports: 55 | - 5775:5775/udp 56 | - 6831:6831/udp 57 | - 6832:6832/udp 58 | - 5778:5778 59 | restart: on-failure -------------------------------------------------------------------------------- /examples/docker-compose.serverless.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | schema: 4 | image: cr.yandex/yc/jaeger-ydb-watcher:dev 5 | environment: 6 | PARTS_IDX_TAG_V2: 4 7 | PARTS_TRACES: 4 8 | PARTS_IDX_SERVICE_OP: 4 9 | PARTS_DURATION: 4 10 | PARTS_IDX_SERVICE_NAME: 4 11 | YDB_SA_META_AUTH: "true" 12 | YDB_CA_FILE: "" 13 | YDB_ADDRESS: ydb.serverless.yandexcloud.net:2135 14 | YDB_PATH: /xxx/yyy/zzz 15 | YDB_FEATURE_COMPRESSION: "true" 16 | YDB_FEATURE_SPLIT_BY_LOAD: "true" 17 | command: watcher 18 | collector: 19 | image: cr.yandex/yc/jaeger-ydb-collector:dev 20 | ports: 21 | - 14269:14269 22 | environment: 23 | YDB_SA_META_AUTH: "true" 24 | YDB_CA_FILE: "" 25 | YDB_ADDRESS: ydb.serverless.yandexcloud.net:2135 26 | YDB_PATH: /xxx/yyy/zzz 27 | YDB_FEATURE_COMPRESSION: "true" 28 | YDB_FEATURE_SPLIT_BY_LOAD: "true" 29 | YDB_WRITER_BUFFER_SIZE: 50000 30 | YDB_WRITER_BATCH_SIZE: 5000 31 | YDB_WRITER_BATCH_WORKERS: 100 32 | YDB_INDEXER_BUFFER_SIZE: 5000 33 | COLLECTOR_QUEUE_SIZE: 50000 34 | PLUGIN_LOG_PATH: /tmp/plugin.log 35 | query: 36 | image: cr.yandex/yc/jaeger-ydb-query:dev 37 | ports: 38 | - 16686:16686 39 | environment: 40 | YDB_SA_META_AUTH: "true" 41 | YDB_CA_FILE: "" 42 | YDB_ADDRESS: ydb.serverless.yandexcloud.net:2135 43 | YDB_PATH: /xxx/yyy/zzz 44 | YDB_FEATURE_COMPRESSION: "true" 45 | YDB_FEATURE_SPLIT_BY_LOAD: "true" 46 | PLUGIN_LOG_PATH: /tmp/plugin.log 47 | agent: 48 | image: jaegertracing/jaeger-agent 49 | command: 50 | - "--reporter.grpc.host-port=collector:14250" 51 | - "--processor.jaeger-binary.server-queue-size=10000" 52 | - "--processor.jaeger-compact.server-queue-size=10000" 53 | - "--processor.zipkin-compact.server-queue-size=10000" 54 | ports: 55 | - 5775:5775/udp 56 | - 6831:6831/udp 57 | - 6832:6832/udp 58 | - 5778:5778 59 | restart: on-failure -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ydb-platform/jaeger-ydb-store 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 7 | github.com/gogo/protobuf v1.3.2 8 | github.com/golang/protobuf v1.5.3 9 | github.com/hashicorp/go-hclog v1.6.2 10 | github.com/hashicorp/golang-lru v1.0.2 11 | github.com/jaegertracing/jaeger v1.54.0 12 | github.com/opentracing/opentracing-go v1.2.0 13 | github.com/prometheus/client_golang v1.18.0 14 | github.com/spf13/cobra v1.8.0 15 | github.com/spf13/pflag v1.0.5 16 | github.com/spf13/viper v1.18.2 17 | github.com/stretchr/testify v1.8.4 18 | github.com/uber/jaeger-lib v2.4.1+incompatible 19 | github.com/ydb-platform/ydb-go-genproto v0.0.0-20240126124512-dbb0e1720dbf 20 | github.com/ydb-platform/ydb-go-sdk-zap v0.16.1 21 | github.com/ydb-platform/ydb-go-sdk/v3 v3.56.1 22 | github.com/ydb-platform/ydb-go-yc v0.12.1 23 | go.uber.org/zap v1.26.0 24 | google.golang.org/grpc v1.61.0 25 | ) 26 | 27 | require ( 28 | github.com/beorn7/perks v1.0.1 // indirect 29 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 30 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 31 | github.com/fatih/color v1.14.1 // indirect 32 | github.com/fsnotify/fsnotify v1.7.0 // indirect 33 | github.com/go-logr/logr v1.4.1 // indirect 34 | github.com/go-logr/stdr v1.2.2 // indirect 35 | github.com/golang-jwt/jwt/v4 v4.5.0 // indirect 36 | github.com/google/uuid v1.5.0 // indirect 37 | github.com/hashicorp/go-plugin v1.6.0 // indirect 38 | github.com/hashicorp/hcl v1.0.0 // indirect 39 | github.com/hashicorp/yamux v0.1.1 // indirect 40 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 41 | github.com/jonboulle/clockwork v0.4.0 // indirect 42 | github.com/magiconair/properties v1.8.7 // indirect 43 | github.com/mattn/go-colorable v0.1.13 // indirect 44 | github.com/mattn/go-isatty v0.0.17 // indirect 45 | github.com/mitchellh/go-testing-interface v1.0.0 // indirect 46 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect 47 | github.com/oklog/run v1.1.0 // indirect 48 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 49 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 50 | github.com/prometheus/client_model v0.5.0 // indirect 51 | github.com/prometheus/common v0.46.0 // indirect 52 | github.com/prometheus/procfs v0.12.0 // indirect 53 | github.com/sagikazarmark/locafero v0.4.0 // indirect 54 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 55 | github.com/sourcegraph/conc v0.3.0 // indirect 56 | github.com/spf13/afero v1.11.0 // indirect 57 | github.com/spf13/cast v1.6.0 // indirect 58 | github.com/subosito/gotenv v1.6.0 // indirect 59 | github.com/yandex-cloud/go-genproto v0.0.0-20211115083454-9ca41db5ed9e // indirect 60 | github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 // indirect 61 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect 62 | go.opentelemetry.io/otel v1.22.0 // indirect 63 | go.opentelemetry.io/otel/metric v1.22.0 // indirect 64 | go.opentelemetry.io/otel/trace v1.22.0 // indirect 65 | go.uber.org/multierr v1.11.0 // indirect 66 | golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect 67 | golang.org/x/net v0.20.0 // indirect 68 | golang.org/x/sync v0.5.0 // indirect 69 | golang.org/x/sys v0.16.0 // indirect 70 | golang.org/x/text v0.14.0 // indirect 71 | google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f // indirect 72 | google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect 73 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect 74 | google.golang.org/protobuf v1.32.0 // indirect 75 | gopkg.in/ini.v1 v1.67.0 // indirect 76 | gopkg.in/yaml.v3 v3.0.1 // indirect 77 | ) 78 | -------------------------------------------------------------------------------- /internal/db/credentials_types.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | type credentialsType int 4 | 5 | const ( 6 | anonymousCredentials = iota 7 | accessTokenCredentials 8 | metadataCredentials 9 | saKeyDeprecatedCredentials 10 | saKeyJsonCredentials 11 | ) 12 | 13 | func (c credentialsType) String() string { 14 | switch c { 15 | case anonymousCredentials: 16 | return "AnonymousCredentials" 17 | case accessTokenCredentials: 18 | return "AccessTokenCredentials" 19 | case metadataCredentials: 20 | return "MetadataCredentials" 21 | case saKeyDeprecatedCredentials: 22 | return "SaKeyDeprecatedCredentials" 23 | case saKeyJsonCredentials: 24 | return "SaKeyJsonCredentials" 25 | default: 26 | return "unspecified" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/db/dialer.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "sort" 8 | "strings" 9 | "time" 10 | 11 | "github.com/spf13/viper" 12 | ydbZap "github.com/ydb-platform/ydb-go-sdk-zap" 13 | "github.com/ydb-platform/ydb-go-sdk/v3" 14 | "github.com/ydb-platform/ydb-go-sdk/v3/credentials" 15 | "github.com/ydb-platform/ydb-go-sdk/v3/table" 16 | "github.com/ydb-platform/ydb-go-sdk/v3/table/types" 17 | "github.com/ydb-platform/ydb-go-sdk/v3/trace" 18 | yc "github.com/ydb-platform/ydb-go-yc" 19 | "go.uber.org/zap" 20 | ) 21 | 22 | const ( 23 | defaultIAMEndpoint = "iam.api.cloud.yandex.net:443" 24 | ) 25 | 26 | type conflictError struct { 27 | gotCreds []credentialsType 28 | } 29 | 30 | func newConflictError(gotCreds []credentialsType) *conflictError { 31 | return &conflictError{ 32 | gotCreds: gotCreds, 33 | } 34 | } 35 | 36 | func (c *conflictError) Error() string { 37 | sort.Slice(c.gotCreds, func(i, j int) bool { 38 | return c.gotCreds[i] < c.gotCreds[j] 39 | }) 40 | template := "conflict error: only 1 credentials type type must be specified, got %d: [%s]" 41 | gotCredsStrBuilder := strings.Builder{} 42 | for i, v := range c.gotCreds { 43 | gotCredsStrBuilder.WriteString(v.String()) 44 | if i != len(c.gotCreds)-1 { 45 | gotCredsStrBuilder.WriteString(", ") 46 | } 47 | } 48 | 49 | return fmt.Sprintf(template, len(c.gotCreds), gotCredsStrBuilder.String()) 50 | } 51 | 52 | var errNotAllSaKeyCredentialsFieldsSpecified = errors.New("not all sa key credentials fields are specified, " + 53 | "need saId, saKeyId, saPrivateKeyFile") 54 | 55 | // isSecureDefault was created to support backward compatibility, 56 | // because previously the type of secure connection was depended on credentials type 57 | func getCredentialsAndSecureType(v *viper.Viper) (creds credentials.Credentials, isSecureDefault bool, err error) { 58 | var gotCreds []credentialsType 59 | 60 | if v.GetBool(keyYdbAnonymous) { 61 | gotCreds = append(gotCreds, anonymousCredentials) 62 | creds = credentials.NewAnonymousCredentials() 63 | isSecureDefault = false 64 | } 65 | 66 | if v.GetString(KeyYdbToken) != "" { 67 | gotCreds = append(gotCreds, accessTokenCredentials) 68 | creds = credentials.NewAccessTokenCredentials(v.GetString(KeyYdbToken)) 69 | isSecureDefault = false 70 | } 71 | 72 | if v.GetBool(KeyYdbSaMetaAuth) { 73 | gotCreds = append(gotCreds, metadataCredentials) 74 | creds = yc.NewInstanceServiceAccount() 75 | isSecureDefault = true 76 | } 77 | 78 | if v.GetString(KeyYdbSaKeyID) != "" || 79 | v.GetString(KeyYdbSaId) != "" || 80 | v.GetString(KeyYdbSaPrivateKeyFile) != "" { 81 | if !(v.GetString(KeyYdbSaKeyID) != "" && 82 | v.GetString(KeyYdbSaId) != "" && 83 | v.GetString(KeyYdbSaPrivateKeyFile) != "") { 84 | return nil, false, errNotAllSaKeyCredentialsFieldsSpecified 85 | } 86 | gotCreds = append(gotCreds, saKeyDeprecatedCredentials) 87 | creds, err = yc.NewClient( 88 | yc.WithKeyID(v.GetString(KeyYdbSaKeyID)), 89 | yc.WithIssuer(v.GetString(KeyYdbSaId)), 90 | yc.WithPrivateKeyFile(v.GetString(KeyYdbSaPrivateKeyFile)), 91 | yc.WithEndpoint(v.GetString(KeyIAMEndpoint)), 92 | yc.WithSystemCertPool(), 93 | ) 94 | if err != nil { 95 | return nil, false, err 96 | } 97 | isSecureDefault = true 98 | } 99 | 100 | if v.GetString(keyYdbSaKeyJson) != "" { 101 | gotCreds = append(gotCreds, saKeyJsonCredentials) 102 | creds, err = yc.NewClient( 103 | yc.WithServiceKey(v.GetString(keyYdbSaKeyJson)), 104 | yc.WithEndpoint(v.GetString(KeyIAMEndpoint)), 105 | yc.WithSystemCertPool(), 106 | ) 107 | if err != nil { 108 | return nil, false, err 109 | } 110 | isSecureDefault = true 111 | } 112 | 113 | if len(gotCreds) != 1 { 114 | return nil, false, newConflictError(gotCreds) 115 | } 116 | 117 | return creds, isSecureDefault, nil 118 | } 119 | 120 | var ErrBadSecureConnectionValue = errors.New("incorrect secure-connection value: must be one of: [enabled, disabled]") 121 | 122 | func GetIsSecureWithDefault(v *viper.Viper, isSecureDefault bool) (isSecure bool, err error) { 123 | switch v.GetString(KeyYdbSecureConnection) { 124 | case "enabled": 125 | isSecure = true 126 | case "disabled": 127 | isSecure = false 128 | case "": 129 | isSecure = isSecureDefault 130 | default: 131 | return false, ErrBadSecureConnectionValue 132 | } 133 | 134 | return isSecure, nil 135 | } 136 | 137 | func options(v *viper.Viper, l *zap.Logger, opts ...ydb.Option) ([]ydb.Option, error) { 138 | v.SetDefault(KeyIAMEndpoint, defaultIAMEndpoint) 139 | if l != nil { 140 | opts = append( 141 | opts, 142 | ydbZap.WithTraces( 143 | l, 144 | trace.MatchDetails( 145 | viper.GetString(KeyYdbLogScope), 146 | trace.WithDefaultDetails( 147 | trace.DiscoveryEvents, 148 | ), 149 | ), 150 | ), 151 | ) 152 | } 153 | 154 | if caFile := v.GetString(KeyYdbCAFile); caFile != "" { 155 | opts = append(opts, ydb.WithCertificatesFromFile(caFile)) 156 | } 157 | creds, isSecureDefault, err := getCredentialsAndSecureType(v) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | isSecure, err := GetIsSecureWithDefault(v, isSecureDefault) 163 | if err != nil { 164 | return nil, err 165 | } 166 | 167 | opts = append( 168 | opts, 169 | ydb.WithCredentials(creds), 170 | ydb.WithSecure(isSecure), 171 | ) 172 | 173 | return opts, nil 174 | } 175 | 176 | func DialFromViper(ctx context.Context, v *viper.Viper, logger *zap.Logger, dsn string, opts ...ydb.Option) (*ydb.Driver, error) { 177 | gotOptions, err := options(v, logger, opts...) 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | return ydb.Open(ctx, dsn, gotOptions...) 183 | } 184 | 185 | func UpsertData(ctx context.Context, pool table.Client, tableName string, rows types.Value, retryAttemptTimeout time.Duration) error { 186 | err := pool.Do( 187 | ctx, 188 | func(ctx context.Context, s table.Session) error { 189 | opCtx := ctx 190 | if retryAttemptTimeout > 0 { 191 | var opCancel context.CancelFunc 192 | opCtx, opCancel = context.WithTimeout(ctx, retryAttemptTimeout) 193 | defer opCancel() 194 | } 195 | return s.BulkUpsert(opCtx, tableName, rows) 196 | }, 197 | table.WithIdempotent(), 198 | ) 199 | return err 200 | } 201 | -------------------------------------------------------------------------------- /internal/db/dialer_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/spf13/viper" 8 | "github.com/stretchr/testify/require" 9 | "github.com/ydb-platform/ydb-go-sdk/v3/credentials" 10 | yc "github.com/ydb-platform/ydb-go-yc" 11 | ) 12 | 13 | func Test_getCredentialsAndSecureType(t *testing.T) { 14 | f, err := os.CreateTemp("", "tmpfile-") 15 | require.NoError(t, err) 16 | t.Cleanup(func() { 17 | _ = f.Close() 18 | _ = os.Remove(f.Name()) 19 | }) 20 | _, err = f.WriteString("-----BEGIN RSA PRIVATE KEY-----\nMCsCAQACBQCIRD8xAgMBAAECBCB4TiUCAwD45wIDAIwnAgJ1ZwICTGsCAjBF\n-----END RSA PRIVATE KEY-----\n") 21 | require.NoError(t, err) 22 | 23 | type input struct { 24 | Env map[string]string 25 | } 26 | type expect struct { 27 | Creds credentials.Credentials 28 | IsSecure bool 29 | HaveError bool 30 | Err error 31 | } 32 | 33 | tests := []struct { 34 | Name string 35 | inputData input 36 | expectData expect 37 | }{ 38 | { 39 | Name: "NoCredsSpecified", 40 | expectData: expect{ 41 | HaveError: true, 42 | Err: &conflictError{gotCreds: []credentialsType{}}, 43 | }, 44 | }, 45 | { 46 | Name: "AnonymousSimple", 47 | inputData: input{ 48 | Env: map[string]string{ 49 | keyYdbAnonymous: "true", 50 | }, 51 | }, 52 | expectData: expect{ 53 | Creds: credentials.NewAnonymousCredentials(), 54 | IsSecure: false, 55 | HaveError: false, 56 | }, 57 | }, 58 | { 59 | Name: "AccessTokenSimple", 60 | inputData: input{ 61 | Env: map[string]string{ 62 | KeyYdbToken: "tokenexample", 63 | }, 64 | }, 65 | expectData: expect{ 66 | Creds: credentials.NewAccessTokenCredentials( 67 | "tokenexample", 68 | ), 69 | IsSecure: false, 70 | HaveError: false, 71 | }, 72 | }, 73 | { 74 | Name: "SaKeyDeprecatedSimple", 75 | inputData: input{ 76 | Env: map[string]string{ 77 | KeyYdbSaId: "biba-id", 78 | KeyYdbSaKeyID: "biba-key-id", 79 | KeyYdbSaPrivateKeyFile: f.Name(), 80 | }, 81 | }, 82 | expectData: expect{ 83 | Creds: func() credentials.Credentials { 84 | creds, err := yc.NewClient( 85 | yc.WithEndpoint(defaultIAMEndpoint), 86 | yc.WithSystemCertPool(), 87 | 88 | yc.WithKeyID("biba-id"), 89 | yc.WithIssuer("biba-sa-id"), 90 | yc.WithPrivateKeyFile(f.Name()), 91 | ) 92 | require.NoError(t, err) 93 | return creds 94 | }(), 95 | IsSecure: true, 96 | HaveError: false, 97 | }, 98 | }, 99 | { 100 | Name: "SaKeyDeprecatedNotAll", 101 | inputData: input{ 102 | Env: map[string]string{ 103 | KeyYdbSaId: "biba-id", 104 | KeyYdbSaPrivateKeyFile: f.Name(), 105 | }, 106 | }, 107 | expectData: expect{ 108 | Creds: func() credentials.Credentials { 109 | creds, err := yc.NewClient( 110 | yc.WithEndpoint(defaultIAMEndpoint), 111 | yc.WithSystemCertPool(), 112 | 113 | yc.WithKeyID("biba-id"), 114 | yc.WithIssuer("biba-sa-id"), 115 | yc.WithPrivateKeyFile(f.Name()), 116 | ) 117 | require.NoError(t, err) 118 | return creds 119 | }(), 120 | IsSecure: true, 121 | HaveError: true, 122 | Err: errNotAllSaKeyCredentialsFieldsSpecified, 123 | }, 124 | }, 125 | { 126 | Name: "SaKeyJsonSimple", 127 | inputData: input{ 128 | Env: map[string]string{ 129 | keyYdbSaKeyJson: ` 130 | { 131 | "id": "biba_id", 132 | "service_account_id": "biba_sa_id", 133 | "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMCsCAQACBQCIRD8xAgMBAAECBCB4TiUCAwD45wIDAIwnAgJ1ZwICTGsCAjBF\n-----END RSA PRIVATE KEY-----\n" 134 | }`, 135 | }, 136 | }, 137 | expectData: expect{ 138 | Creds: func() credentials.Credentials { 139 | keyClientOption := yc.WithServiceKey(` 140 | { 141 | "id": "biba_id", 142 | "service_account_id": "biba_sa_id", 143 | "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMCsCAQACBQCIRD8xAgMBAAECBCB4TiUCAwD45wIDAIwnAgJ1ZwICTGsCAjBF\n-----END RSA PRIVATE KEY-----\n" 144 | }`) 145 | creds, err := yc.NewClient( 146 | keyClientOption, 147 | yc.WithEndpoint(defaultIAMEndpoint), 148 | yc.WithSystemCertPool(), 149 | ) 150 | require.NoError(t, err) 151 | return creds 152 | }(), 153 | IsSecure: true, 154 | HaveError: false, 155 | }, 156 | }, 157 | { 158 | Name: "Conflict", 159 | inputData: input{ 160 | Env: map[string]string{ 161 | keyYdbAnonymous: "true", 162 | KeyYdbToken: "tokenexample", 163 | }, 164 | }, 165 | expectData: expect{ 166 | HaveError: true, 167 | Err: &conflictError{ 168 | gotCreds: []credentialsType{anonymousCredentials, accessTokenCredentials}, 169 | }, 170 | }, 171 | }, 172 | } 173 | 174 | for _, tt := range tests { 175 | tt := tt 176 | t.Run(tt.Name, func(t *testing.T) { 177 | v := viper.New() 178 | v.Set(KeyIAMEndpoint, defaultIAMEndpoint) 179 | for key, value := range tt.inputData.Env { 180 | v.Set(key, value) 181 | } 182 | 183 | _, isSecure, err := getCredentialsAndSecureType(v) 184 | if tt.expectData.HaveError { 185 | require.Equal(t, tt.expectData.Err.Error(), err.Error()) 186 | } else { 187 | require.NoError(t, err) 188 | require.Equal(t, tt.expectData.IsSecure, isSecure) 189 | } 190 | }) 191 | } 192 | } 193 | 194 | func TestGetIsSecureWithDefault(t *testing.T) { 195 | type input struct { 196 | KeySecureConnection string 197 | IsSecureDefault bool 198 | } 199 | type expect struct { 200 | IsSecure bool 201 | HaveError bool 202 | Err error 203 | } 204 | 205 | tests := []struct { 206 | Name string 207 | inputData input 208 | expectData expect 209 | }{ 210 | { 211 | Name: "Enabled", 212 | inputData: input{ 213 | KeySecureConnection: "enabled", 214 | IsSecureDefault: false, 215 | }, 216 | expectData: expect{ 217 | IsSecure: true, 218 | HaveError: false, 219 | Err: nil, 220 | }, 221 | }, 222 | { 223 | Name: "Disabled", 224 | inputData: input{ 225 | KeySecureConnection: "disabled", 226 | IsSecureDefault: false, 227 | }, 228 | expectData: expect{ 229 | IsSecure: false, 230 | HaveError: false, 231 | Err: nil, 232 | }, 233 | }, 234 | { 235 | Name: "Default", 236 | inputData: input{ 237 | KeySecureConnection: "", 238 | IsSecureDefault: true, 239 | }, 240 | expectData: expect{ 241 | IsSecure: true, 242 | HaveError: false, 243 | Err: nil, 244 | }, 245 | }, 246 | { 247 | Name: "ErrorBadValue", 248 | inputData: input{ 249 | KeySecureConnection: "bad value", 250 | IsSecureDefault: false, 251 | }, 252 | expectData: expect{ 253 | IsSecure: false, 254 | HaveError: true, 255 | Err: ErrBadSecureConnectionValue, 256 | }, 257 | }, 258 | } 259 | 260 | for _, tt := range tests { 261 | tt := tt 262 | t.Run(tt.Name, func(t *testing.T) { 263 | v := viper.New() 264 | v.Set(KeyYdbSecureConnection, tt.inputData.KeySecureConnection) 265 | isSecure, err := GetIsSecureWithDefault(v, tt.inputData.IsSecureDefault) 266 | if tt.expectData.HaveError { 267 | require.ErrorIs(t, err, tt.expectData.Err) 268 | return 269 | } 270 | require.NoError(t, err) 271 | require.Equal(t, tt.expectData.IsSecure, isSecure) 272 | }) 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /internal/db/errors.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/ydb-platform/ydb-go-genproto/protos/Ydb" 7 | "github.com/ydb-platform/ydb-go-sdk/v3" 8 | ) 9 | 10 | func IssueContainsMessage(err error, search string) bool { 11 | result := false 12 | ydb.IterateByIssues(err, func(message string, code Ydb.StatusIds_StatusCode, severity uint32) { 13 | if strings.Contains(message, search) { 14 | result = true 15 | } 16 | }) 17 | return result 18 | } 19 | -------------------------------------------------------------------------------- /internal/db/viper.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | const ( 4 | KeyYdbAddress = "ydb.address" 5 | KeyYdbPath = "ydb.path" 6 | KeyYdbFolder = "ydb.folder" 7 | // enabled or disabled 8 | KeyYdbSecureConnection = "ydb.secure-connection" 9 | 10 | keyYdbAnonymous = "ydb.anonymous" 11 | KeyYdbToken = "ydb.token" 12 | KeyYdbSaMetaAuth = "ydb.sa.meta-auth" 13 | keyYdbSaKeyJson = "ydb.Sa.Key-json" 14 | 15 | // Deprecated: now part of keyYdbSaKeyJson 16 | KeyYdbSaPrivateKeyFile = "ydb.sa.private-key-file" 17 | // Deprecated: now part of keyYdbSaKeyJson 18 | KeyYdbSaId = "ydb.sa.id" 19 | // Deprecated: now part of keyYdbSaKeyJson 20 | KeyYdbSaKeyID = "ydb.sa.key-id" 21 | 22 | KeyYdbCAFile = "ydb.ca-file" 23 | 24 | KeyYdbConnectTimeout = "ydb.connect-timeout" 25 | KeyYdbWriteTimeout = "ydb.write-timeout" 26 | KeyYdbRetryAttemptTimeout = "ydb.retry-attempt-timeout" 27 | 28 | KeyYdbReadTimeout = "ydb.read-timeout" 29 | KeyYdbReadQueryParallel = "ydb.read-query-parallel" 30 | KeyYdbReadOpLimit = "ydb.read-op-limit" 31 | KeyYdbReadSvcLimit = "ydb.read-svc-limit" 32 | 33 | KeyYdbPoolSize = "ydb.pool-size" 34 | 35 | KeyYdbQueryCacheSize = "ydb.query-cache-size" 36 | 37 | KeyYdbWriterBufferSize = "ydb.writer.buffer-size" 38 | KeyYdbWriterBatchSize = "ydb.writer.batch-size" 39 | KeyYdbWriterBatchWorkers = "ydb.writer.batch-workers" 40 | // KeyYdbWriterMaxSpanAge controls max age for accepted spans. 41 | // Each span older than time.Now() - KeyYdbWriterMaxSpanAge will be neglected. 42 | // Defaults to zero which effectively means any span age is good. 43 | KeyYdbWriterMaxSpanAge = "ydb.writer.max-span-age" 44 | KeyYdbWriterSvcOpCacheSize = "ydb.writer.service-name-operation-cache-size" 45 | 46 | KeyYdbIndexerBufferSize = "ydb.indexer.buffer-size" 47 | KeyYdbIndexerMaxTraces = "ydb.indexer.max-traces" 48 | KeyYdbIndexerMaxTTL = "ydb.indexer.max-ttl" 49 | 50 | KeyYDBPartitionSize = "ydb.partition-size" 51 | KeyYDBFeatureSplitByLoad = "ydb.feature.split-by-load" 52 | KeyYDBFeatureCompression = "ydb.feature.compression" 53 | 54 | KeyYdbLogScope = "ydb.log.scope" 55 | ) 56 | 57 | const ( 58 | KeyIAMEndpoint = "iam.endpoint" 59 | ) 60 | -------------------------------------------------------------------------------- /internal/testutil/db_helper.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math" 7 | "os" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/require" 13 | "github.com/ydb-platform/ydb-go-sdk/v3" 14 | "github.com/ydb-platform/ydb-go-sdk/v3/sugar" 15 | "github.com/ydb-platform/ydb-go-sdk/v3/table" 16 | 17 | "github.com/ydb-platform/jaeger-ydb-store/schema" 18 | ) 19 | 20 | var ( 21 | db struct { 22 | once sync.Once 23 | pool table.Client 24 | done bool 25 | } 26 | 27 | defaultTXC = table.TxControl( 28 | table.BeginTx(table.WithSerializableReadWrite()), 29 | table.CommitTx(), 30 | ) 31 | ) 32 | 33 | func initDb(tb testing.TB) { 34 | db.once.Do(func() { 35 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) 36 | defer cancel() 37 | dbPath := schema.DbPath{Path: os.Getenv("YDB_PATH"), Folder: os.Getenv("YDB_FOLDER")} 38 | require.NotEmpty(tb, os.Getenv("YDB_ADDRESS")) 39 | conn, err := ydb.Open(ctx, 40 | sugar.DSN(os.Getenv("YDB_ADDRESS"), dbPath.Path, os.Getenv("YDB_SECURE") == "1"), 41 | ydb.WithSessionPoolSizeLimit(10), 42 | ydb.WithAccessTokenCredentials(os.Getenv("YDB_TOKEN")), 43 | ) 44 | if err != nil { 45 | tb.Fatalf("ydb connect failed: %v", err) 46 | } 47 | db.pool = conn.Table() 48 | 49 | err = conn.Table().Do(ctx, func(ctx context.Context, session table.Session) error { 50 | return CreateTables(ctx, dbPath, session) 51 | }) 52 | if err != nil { 53 | tb.Fatalf("failed to create static tables: %v", err) 54 | } 55 | err = conn.Table().Do(ctx, func(ctx context.Context, session table.Session) error { 56 | _, _, err := session.Execute(ctx, defaultTXC, schema.BuildQuery(dbPath, schema.DeleteAllParts), nil) 57 | return err 58 | }) 59 | if err != nil { 60 | tb.Fatalf("failed to clean part table: %v", err) 61 | } 62 | 63 | err = CreatePartitionTables(ctx, conn.Table(), partRange(time.Now(), time.Now().Add(time.Hour*2))...) 64 | if err != nil { 65 | tb.Fatalf("failed to create partition tables: %v", err) 66 | } 67 | 68 | db.done = true 69 | }) 70 | if !db.done { 71 | tb.Fatal("initDb failed") 72 | } 73 | } 74 | 75 | func YdbSessionPool(tb testing.TB) table.Client { 76 | initDb(tb) 77 | return db.pool 78 | } 79 | 80 | func partRange(start, stop time.Time) []schema.PartitionKey { 81 | result := make([]schema.PartitionKey, 0) 82 | diff := stop.Sub(start) 83 | if diff < 0 { 84 | panic("stop < start") 85 | } 86 | numParts := int(math.Floor(diff.Hours())) + 1 87 | for i := 0; i < numParts; i++ { 88 | result = append(result, schema.PartitionFromTime(start.Add(time.Hour*time.Duration(i)))) 89 | } 90 | return result 91 | } 92 | 93 | func CreateTables(ctx context.Context, dbPath schema.DbPath, session table.Session) error { 94 | for name, definition := range schema.Tables { 95 | fullPath := dbPath.FullTable(name) 96 | if err := session.CreateTable(ctx, fullPath, definition()...); err != nil { 97 | return err 98 | } 99 | } 100 | return nil 101 | } 102 | 103 | func CreatePartitionTables(ctx context.Context, tc table.Client, parts ...schema.PartitionKey) error { 104 | var err error 105 | dbPath := schema.DbPath{Path: os.Getenv("YDB_PATH"), Folder: os.Getenv("YDB_FOLDER")} 106 | 107 | for _, part := range parts { 108 | err = tc.Do(ctx, func(ctx context.Context, session table.Session) error { 109 | _, _, err = session.Execute(ctx, defaultTXC, schema.BuildQuery(dbPath, schema.InsertPart), part.QueryParams()) 110 | return err 111 | }) 112 | if err != nil { 113 | return fmt.Errorf("failed to insert part '%+v': %v", part, err) 114 | } 115 | for name, tableOptions := range schema.PartitionTables { 116 | fullPath := part.BuildFullTableName(dbPath.String(), name) 117 | tbl := dbPath.Table(name) 118 | err = tc.Do(ctx, func(ctx context.Context, session table.Session) error { 119 | return session.CreateTable(ctx, fullPath, tableOptions(3)...) 120 | }) 121 | if err != nil { 122 | return fmt.Errorf("failed to create table '%s': %v", fullPath, err) 123 | } 124 | 125 | err = tc.Do(ctx, func(ctx context.Context, session table.Session) error { 126 | _, _, err = session.Execute(ctx, defaultTXC, fmt.Sprintf("DELETE FROM `%s_%s`", tbl, part.Suffix()), table.NewQueryParameters()) 127 | return err 128 | }) 129 | if err != nil { 130 | return fmt.Errorf("failed to clean table '%s': %v", fullPath, err) 131 | } 132 | } 133 | } 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /internal/testutil/logger.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/hashicorp/go-hclog" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | func Zap() *zap.Logger { 11 | if os.Getenv("TEST_LOGGING") == "1" { 12 | logger, _ := zap.NewDevelopment() 13 | return logger 14 | } 15 | return zap.NewNop() 16 | } 17 | 18 | func JaegerLogger() hclog.Logger { 19 | return hclog.NewNullLogger() 20 | } 21 | -------------------------------------------------------------------------------- /internal/testutil/traces.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/jaegertracing/jaeger/model" 8 | ) 9 | 10 | var r = rand.New(rand.NewSource(time.Now().UnixNano())) 11 | 12 | func GenerateTraceID() model.TraceID { 13 | return model.TraceID{High: r.Uint64(), Low: r.Uint64()} 14 | } 15 | -------------------------------------------------------------------------------- /internal/viper/viper.go: -------------------------------------------------------------------------------- 1 | package viper 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/spf13/pflag" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | // ConfigureViperFromFlag reads --config flag and attempts to setup v. 13 | func ConfigureViperFromFlag(v *viper.Viper) { 14 | cmd := pflag.NewFlagSet(os.Args[0], pflag.ExitOnError) 15 | path := cmd.String("config", "", "full path to configuration file") 16 | // Ignore errors; cmd is set for ExitOnError. 17 | _ = cmd.Parse(os.Args[1:]) 18 | 19 | if len(*path) > 0 { 20 | extension := filepath.Ext(*path) 21 | if len(extension) == 0 { 22 | log.Fatal("Cannot find file extension in path", *path) 23 | } 24 | extension = extension[1:] 25 | v.SetConfigType(extension) 26 | 27 | f, err := os.Open(*path) 28 | if err != nil { 29 | log.Fatal("Could not open file", *path) 30 | } 31 | err = v.ReadConfig(f) 32 | if err != nil { 33 | log.Fatal("Could not read config file", *path) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/pprof" 7 | "os" 8 | "strings" 9 | 10 | "github.com/hashicorp/go-hclog" 11 | jaegerGrpc "github.com/jaegertracing/jaeger/plugin/storage/grpc" 12 | "github.com/jaegertracing/jaeger/plugin/storage/grpc/shared" 13 | "github.com/prometheus/client_golang/prometheus" 14 | "github.com/prometheus/client_golang/prometheus/promhttp" 15 | "github.com/spf13/viper" 16 | 17 | localViper "github.com/ydb-platform/jaeger-ydb-store/internal/viper" 18 | "github.com/ydb-platform/jaeger-ydb-store/plugin" 19 | ) 20 | 21 | func init() { 22 | viper.SetDefault("plugin_http_listen_address", ":15000") 23 | viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) 24 | viper.AutomaticEnv() 25 | } 26 | 27 | func newJaegerLogger() hclog.Logger { 28 | pluginLogger := hclog.New(&hclog.LoggerOptions{ 29 | Name: "ydb-store-plugin", 30 | JSONFormat: true, 31 | }) 32 | 33 | return pluginLogger 34 | } 35 | 36 | func main() { 37 | localViper.ConfigureViperFromFlag(viper.GetViper()) 38 | 39 | jaegerLogger := newJaegerLogger() 40 | ctx, cancel := context.WithCancel(context.Background()) 41 | defer cancel() 42 | 43 | ydbPlugin, err := plugin.NewYdbStorage(ctx, viper.GetViper(), jaegerLogger) 44 | if err != nil { 45 | jaegerLogger.Error(err.Error()) 46 | os.Exit(1) 47 | } 48 | defer ydbPlugin.Close() 49 | 50 | go serveHttp(ydbPlugin.Registry(), jaegerLogger) 51 | 52 | jaegerLogger.Warn("starting plugin") 53 | jaegerGrpc.Serve(&shared.PluginServices{ 54 | Store: ydbPlugin, 55 | ArchiveStore: ydbPlugin, 56 | }) 57 | jaegerLogger.Warn("stopped") 58 | } 59 | 60 | func serveHttp(gatherer prometheus.Gatherer, jaegerLogger hclog.Logger) { 61 | mux := http.NewServeMux() 62 | jaegerLogger.Warn("serving metrics", "addr", viper.GetString("plugin_http_listen_address")) 63 | mux.Handle("/metrics", promhttp.HandlerFor(gatherer, promhttp.HandlerOpts{})) 64 | mux.HandleFunc("/ping", func(writer http.ResponseWriter, request *http.Request) { 65 | writer.WriteHeader(http.StatusOK) 66 | }) 67 | 68 | if viper.GetBool("ENABLE_PPROF") { 69 | mux.HandleFunc("/debug/pprof/", pprof.Index) 70 | mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 71 | mux.HandleFunc("/debug/pprof/trace", pprof.Trace) 72 | } 73 | 74 | err := http.ListenAndServe(viper.GetString("plugin_http_listen_address"), mux) 75 | if err != nil { 76 | jaegerLogger.Error("failed to start http listener", "err", err) 77 | os.Exit(1) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /plugin/metrics.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/uber/jaeger-lib/metrics" 7 | "github.com/ydb-platform/ydb-go-sdk/v3/trace" 8 | ) 9 | 10 | func tableClientMetrics(factory metrics.Factory) trace.Table { 11 | m := map[string]struct{}{} 12 | ns := factory.Namespace(metrics.NSOptions{Name: "tc"}) 13 | sc := ns.Gauge(metrics.Options{Name: "sessions"}) 14 | mx := new(sync.Mutex) 15 | return trace.Table{ 16 | OnSessionNew: func(trace.TableSessionNewStartInfo) func(trace.TableSessionNewDoneInfo) { 17 | return func(doneInfo trace.TableSessionNewDoneInfo) { 18 | mx.Lock() 19 | defer mx.Unlock() 20 | if doneInfo.Error == nil { 21 | m[doneInfo.Session.ID()] = struct{}{} 22 | sc.Update(int64(len(m))) 23 | } 24 | } 25 | }, 26 | OnSessionDelete: func(info trace.TableSessionDeleteStartInfo) func(trace.TableSessionDeleteDoneInfo) { 27 | return func(_ trace.TableSessionDeleteDoneInfo) { 28 | mx.Lock() 29 | defer mx.Unlock() 30 | delete(m, info.Session.ID()) 31 | sc.Update(int64(len(m))) 32 | } 33 | }, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/hashicorp/go-hclog" 9 | "github.com/jaegertracing/jaeger/storage/dependencystore" 10 | "github.com/jaegertracing/jaeger/storage/spanstore" 11 | "github.com/prometheus/client_golang/prometheus" 12 | "github.com/spf13/viper" 13 | "github.com/uber/jaeger-lib/metrics" 14 | jgrProm "github.com/uber/jaeger-lib/metrics/prometheus" 15 | "github.com/ydb-platform/ydb-go-sdk/v3" 16 | "github.com/ydb-platform/ydb-go-sdk/v3/sugar" 17 | "github.com/ydb-platform/ydb-go-sdk/v3/table" 18 | "go.uber.org/zap" 19 | 20 | "github.com/ydb-platform/jaeger-ydb-store/internal/db" 21 | "github.com/ydb-platform/jaeger-ydb-store/schema" 22 | "github.com/ydb-platform/jaeger-ydb-store/storage/config" 23 | ydbDepStore "github.com/ydb-platform/jaeger-ydb-store/storage/dependencystore" 24 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/reader" 25 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/writer" 26 | ) 27 | 28 | type YdbStorage struct { 29 | metricsFactory metrics.Factory 30 | metricsRegistry *prometheus.Registry 31 | logger *zap.Logger 32 | jaegerLogger hclog.Logger 33 | ydbPool table.Client 34 | opts config.Options 35 | 36 | writer *writer.SpanWriter 37 | reader *reader.SpanReader 38 | archiveWriter *writer.SpanWriter 39 | archiveReader *reader.SpanReader 40 | } 41 | 42 | func NewYdbStorage(ctx context.Context, v *viper.Viper, jaegerLogger hclog.Logger) (*YdbStorage, error) { 43 | v.SetDefault(db.KeyYdbConnectTimeout, time.Second*10) 44 | v.SetDefault(db.KeyYdbWriterBufferSize, 1000) 45 | v.SetDefault(db.KeyYdbWriterBatchSize, 100) 46 | v.SetDefault(db.KeyYdbWriterBatchWorkers, 10) 47 | v.SetDefault(db.KeyYdbWriterSvcOpCacheSize, 256) 48 | v.SetDefault(db.KeyYdbIndexerBufferSize, 1000) 49 | v.SetDefault(db.KeyYdbIndexerMaxTraces, 100) 50 | v.SetDefault(db.KeyYdbIndexerMaxTTL, time.Second*5) 51 | v.SetDefault(db.KeyYdbPoolSize, 100) 52 | v.SetDefault(db.KeyYdbQueryCacheSize, 50) 53 | v.SetDefault(db.KeyYdbReadTimeout, time.Second*10) 54 | v.SetDefault(db.KeyYdbReadQueryParallel, 16) 55 | v.SetDefault(db.KeyYdbReadOpLimit, 5000) 56 | v.SetDefault(db.KeyYdbReadSvcLimit, 1000) 57 | // Zero stands for "unbound" interval so any span age is good. 58 | v.SetDefault(db.KeyYdbWriterMaxSpanAge, time.Duration(0)) 59 | 60 | registry := prometheus.NewRegistry() 61 | 62 | p := &YdbStorage{ 63 | metricsRegistry: registry, 64 | metricsFactory: jgrProm.New(jgrProm.WithRegisterer(registry)).Namespace(metrics.NSOptions{Name: "jaeger_ydb"}), 65 | } 66 | 67 | p.opts = config.Options{ 68 | DbAddress: v.GetString(db.KeyYdbAddress), 69 | DbPath: schema.DbPath{ 70 | Path: v.GetString(db.KeyYdbPath), 71 | Folder: v.GetString(db.KeyYdbFolder), 72 | }, 73 | PoolSize: v.GetInt(db.KeyYdbPoolSize), 74 | QueryCacheSize: v.GetInt(db.KeyYdbQueryCacheSize), 75 | ConnectTimeout: v.GetDuration(db.KeyYdbConnectTimeout), 76 | BufferSize: v.GetInt(db.KeyYdbWriterBufferSize), 77 | BatchSize: v.GetInt(db.KeyYdbWriterBatchSize), 78 | BatchWorkers: v.GetInt(db.KeyYdbWriterBatchWorkers), 79 | WriteSvcOpCacheSize: v.GetInt(db.KeyYdbWriterSvcOpCacheSize), 80 | IndexerBufferSize: v.GetInt(db.KeyYdbIndexerBufferSize), 81 | IndexerMaxTraces: v.GetInt(db.KeyYdbIndexerMaxTraces), 82 | IndexerMaxTTL: v.GetDuration(db.KeyYdbIndexerMaxTTL), 83 | WriteTimeout: v.GetDuration(db.KeyYdbWriteTimeout), 84 | RetryAttemptTimeout: v.GetDuration(db.KeyYdbRetryAttemptTimeout), 85 | ReadTimeout: v.GetDuration(db.KeyYdbReadTimeout), 86 | ReadQueryParallel: v.GetInt(db.KeyYdbReadQueryParallel), 87 | ReadOpLimit: v.GetUint64(db.KeyYdbReadOpLimit), 88 | ReadSvcLimit: v.GetUint64(db.KeyYdbReadSvcLimit), 89 | WriteMaxSpanAge: v.GetDuration(db.KeyYdbWriterMaxSpanAge), 90 | } 91 | 92 | cfg := zap.NewProductionConfig() 93 | if logPath := v.GetString("plugin_log_path"); logPath != "" { 94 | cfg.ErrorOutputPaths = []string{logPath} 95 | cfg.OutputPaths = []string{logPath} 96 | } 97 | var err error 98 | p.logger, err = cfg.Build() 99 | if err != nil { 100 | return nil, fmt.Errorf("NewYdbStorage(): %w", err) 101 | } 102 | 103 | p.jaegerLogger = jaegerLogger 104 | 105 | p.ydbPool, err = p.connectToYDB(ctx, v) 106 | if err != nil { 107 | return nil, fmt.Errorf("NewYdbStorage(): %w", err) 108 | } 109 | 110 | p.writer = p.createWriter() 111 | p.archiveWriter = p.createArchiveWriter() 112 | 113 | p.reader = p.createReader() 114 | p.archiveReader = p.createArchiveReader() 115 | 116 | return p, nil 117 | } 118 | 119 | func (p *YdbStorage) Registry() *prometheus.Registry { 120 | return p.metricsRegistry 121 | } 122 | 123 | func (p *YdbStorage) SpanReader() spanstore.Reader { 124 | return p.reader 125 | } 126 | 127 | func (p *YdbStorage) SpanWriter() spanstore.Writer { 128 | return p.writer 129 | } 130 | 131 | func (p *YdbStorage) ArchiveSpanReader() spanstore.Reader { 132 | return p.archiveReader 133 | } 134 | 135 | func (p *YdbStorage) ArchiveSpanWriter() spanstore.Writer { 136 | return p.archiveWriter 137 | } 138 | 139 | func (*YdbStorage) DependencyReader() dependencystore.Reader { 140 | return ydbDepStore.DependencyStore{} 141 | } 142 | 143 | func (p *YdbStorage) connectToYDB(ctx context.Context, v *viper.Viper) (table.Client, error) { 144 | ctx, cancel := context.WithTimeout(ctx, p.opts.ConnectTimeout) 145 | defer cancel() 146 | 147 | conn, err := db.DialFromViper( 148 | ctx, 149 | v, 150 | p.logger, 151 | sugar.DSN(p.opts.DbAddress, p.opts.DbPath.Path, true), 152 | ydb.WithSessionPoolSizeLimit(p.opts.PoolSize), 153 | ydb.WithSessionPoolKeepAliveTimeout(time.Second), 154 | ydb.WithTraceTable(tableClientMetrics(p.metricsFactory)), 155 | ) 156 | if err != nil { 157 | return nil, fmt.Errorf("YdbStorage.InitDB() %w", err) 158 | } 159 | 160 | err = conn.Table().Do( 161 | ctx, 162 | func(ctx context.Context, s table.Session) error { 163 | return s.KeepAlive(ctx) 164 | }, 165 | ) 166 | if err != nil { 167 | return nil, fmt.Errorf("YdbStorage.InitDB() %w", err) 168 | } 169 | 170 | return conn.Table(), nil 171 | } 172 | 173 | func (p *YdbStorage) createWriter() *writer.SpanWriter { 174 | opts := writer.SpanWriterOptions{ 175 | BufferSize: p.opts.BufferSize, 176 | BatchSize: p.opts.BatchSize, 177 | BatchWorkers: p.opts.BatchWorkers, 178 | IndexerBufferSize: p.opts.IndexerBufferSize, 179 | IndexerMaxTraces: p.opts.IndexerMaxTraces, 180 | IndexerTTL: p.opts.IndexerMaxTTL, 181 | DbPath: p.opts.DbPath, 182 | WriteTimeout: p.opts.WriteTimeout, 183 | RetryAttemptTimeout: p.opts.RetryAttemptTimeout, 184 | OpCacheSize: p.opts.WriteSvcOpCacheSize, 185 | MaxSpanAge: p.opts.WriteMaxSpanAge, 186 | } 187 | ns := p.metricsFactory.Namespace(metrics.NSOptions{Name: "writer"}) 188 | w := writer.NewSpanWriter(p.ydbPool, ns, p.logger, p.jaegerLogger, opts) 189 | return w 190 | } 191 | 192 | func (p *YdbStorage) createArchiveWriter() *writer.SpanWriter { 193 | opts := writer.SpanWriterOptions{ 194 | ArchiveWriter: true, 195 | BufferSize: p.opts.BufferSize, 196 | BatchSize: p.opts.BatchSize, 197 | BatchWorkers: p.opts.BatchWorkers, 198 | IndexerBufferSize: p.opts.IndexerBufferSize, 199 | IndexerMaxTraces: p.opts.IndexerMaxTraces, 200 | IndexerTTL: p.opts.IndexerMaxTTL, 201 | DbPath: p.opts.DbPath, 202 | WriteTimeout: p.opts.WriteTimeout, 203 | RetryAttemptTimeout: p.opts.RetryAttemptTimeout, 204 | OpCacheSize: p.opts.WriteSvcOpCacheSize, 205 | MaxSpanAge: p.opts.WriteMaxSpanAge, 206 | } 207 | ns := p.metricsFactory.Namespace(metrics.NSOptions{Name: "writer"}) 208 | w := writer.NewSpanWriter(p.ydbPool, ns, p.logger, p.jaegerLogger, opts) 209 | return w 210 | } 211 | 212 | func (p *YdbStorage) createReader() *reader.SpanReader { 213 | opts := reader.SpanReaderOptions{ 214 | DbPath: p.opts.DbPath, 215 | ReadTimeout: p.opts.ReadTimeout, 216 | QueryParallel: p.opts.ReadQueryParallel, 217 | OpLimit: p.opts.ReadOpLimit, 218 | SvcLimit: p.opts.ReadSvcLimit, 219 | } 220 | r := reader.NewSpanReader(p.ydbPool, opts, p.logger, p.jaegerLogger) 221 | return r 222 | } 223 | 224 | func (p *YdbStorage) createArchiveReader() *reader.SpanReader { 225 | opts := reader.SpanReaderOptions{ 226 | ArchiveReader: true, 227 | DbPath: p.opts.DbPath, 228 | ReadTimeout: p.opts.ReadTimeout, 229 | QueryParallel: p.opts.ReadQueryParallel, 230 | OpLimit: p.opts.ReadOpLimit, 231 | SvcLimit: p.opts.ReadSvcLimit, 232 | } 233 | r := reader.NewSpanReader(p.ydbPool, opts, p.logger, p.jaegerLogger) 234 | return r 235 | } 236 | 237 | func (p *YdbStorage) Close() { 238 | p.writer.Close() 239 | p.archiveWriter.Close() 240 | } 241 | -------------------------------------------------------------------------------- /schema/db.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "path" 5 | ) 6 | 7 | type DbPath struct { 8 | Path string 9 | Folder string 10 | } 11 | 12 | func (p DbPath) String() string { 13 | return path.Join(p.Path, p.Folder) 14 | } 15 | 16 | // FullTable returns full table name 17 | func (p DbPath) FullTable(name string) string { 18 | return path.Join(p.Path, p.Folder, name) 19 | } 20 | 21 | func (p DbPath) Table(name string) string { 22 | return path.Join(p.Folder, name) 23 | } 24 | -------------------------------------------------------------------------------- /schema/partition.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "math" 5 | "os" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/ydb-platform/ydb-go-sdk/v3/table" 11 | "github.com/ydb-platform/ydb-go-sdk/v3/table/types" 12 | ) 13 | 14 | const ( 15 | partitionDateFormat = "20060102" 16 | ) 17 | 18 | var ( 19 | numPartitions = 10 20 | partitionStep = time.Hour * 24 / 10 21 | ) 22 | 23 | func init() { 24 | // TODO: should probably rewrite partition helper funcs as some sort of schema builder object 25 | if v, err := strconv.Atoi(os.Getenv("YDB_SCHEMA_NUM_PARTITIONS")); err == nil { 26 | numPartitions = v 27 | partitionStep = time.Hour * 24 / time.Duration(numPartitions) 28 | } 29 | } 30 | 31 | type PartitionKey struct { 32 | Date string 33 | Num uint8 34 | IsActive bool 35 | } 36 | 37 | func (k PartitionKey) Suffix() string { 38 | w := new(strings.Builder) 39 | w.WriteString(k.Date) 40 | w.WriteString("_") 41 | w.WriteString(strconv.FormatInt(int64(k.Num), 10)) 42 | return w.String() 43 | } 44 | 45 | func (k PartitionKey) QueryWhereParams() *table.QueryParameters { 46 | return table.NewQueryParameters( 47 | table.ValueParam("$part_date", types.TextValue(k.Date)), 48 | table.ValueParam("$part_num", types.Uint8Value(uint8(k.Num))), 49 | ) 50 | } 51 | 52 | func (k PartitionKey) QueryParams() *table.QueryParameters { 53 | return table.NewQueryParameters( 54 | table.ValueParam("$part_date", types.TextValue(k.Date)), 55 | table.ValueParam("$part_num", types.Uint8Value(uint8(k.Num))), 56 | table.ValueParam("$is_active", types.BoolValue(k.IsActive)), 57 | ) 58 | } 59 | 60 | func (k PartitionKey) BuildFullTableName(dbPath, table string) string { 61 | sb := new(strings.Builder) 62 | sb.WriteString(dbPath) 63 | sb.WriteString("/") 64 | sb.WriteString(table) 65 | sb.WriteString("_") 66 | sb.WriteString(k.Date) 67 | sb.WriteString("_") 68 | sb.WriteString(strconv.FormatInt(int64(k.Num), 10)) 69 | return sb.String() 70 | } 71 | 72 | func (k PartitionKey) TimeSpan() (begin time.Time, end time.Time) { 73 | t, err := time.Parse(partitionDateFormat, k.Date) 74 | if err != nil { 75 | return 76 | } 77 | begin = t.Add(time.Duration(k.Num) * partitionStep) 78 | end = t.Add(time.Duration(k.Num+1) * partitionStep) 79 | return 80 | } 81 | 82 | func PartitionFromTime(t time.Time) PartitionKey { 83 | hours := t.UTC().Sub(t.Truncate(time.Hour * 24)).Hours() 84 | return PartitionKey{ 85 | Date: t.UTC().Format(partitionDateFormat), 86 | Num: uint8(hours * float64(numPartitions) / 24), 87 | IsActive: true, 88 | } 89 | } 90 | 91 | func MakePartitionList(start, end time.Time) []PartitionKey { 92 | cur := start.Truncate(partitionStep) 93 | n := int(end.Sub(cur)/partitionStep) + 1 94 | retMe := make([]PartitionKey, 0, n) 95 | for end.After(cur) || end.Equal(cur) { 96 | retMe = append(retMe, PartitionFromTime(cur)) 97 | cur = cur.Add(partitionStep) 98 | } 99 | return retMe 100 | } 101 | 102 | func IntersectPartList(a, b []PartitionKey) []PartitionKey { 103 | hash := make(map[PartitionKey]struct{}, len(a)) 104 | set := make([]PartitionKey, 0, int(math.Min(float64(len(a)), float64(len(b))))) 105 | 106 | for _, el := range a { 107 | hash[el] = struct{}{} 108 | } 109 | for _, el := range b { 110 | if _, found := hash[el]; found { 111 | set = append(set, el) 112 | } 113 | } 114 | return set 115 | } 116 | -------------------------------------------------------------------------------- /schema/partition_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestPartitionKey_TimeSpan(t *testing.T) { 11 | ts := time.Date(2063, 4, 5, 1, 0, 0, 0, time.UTC) 12 | b := PartitionFromTime(ts) 13 | bStart, bEnd := b.TimeSpan() 14 | assert.NotEqual(t, bEnd, bStart) 15 | diff := bEnd.Sub(bStart) 16 | assert.Equal(t, partitionStep, diff) 17 | } 18 | 19 | func TestMakePartitionList(t *testing.T) { 20 | t.Run("basic", func(t *testing.T) { 21 | ts := time.Now() 22 | result := MakePartitionList(ts, ts.Add(time.Nanosecond)) 23 | assert.Len(t, result, 1) 24 | }) 25 | t.Run("sametime", func(t *testing.T) { 26 | ts := time.Now() 27 | result := MakePartitionList(ts, ts) 28 | assert.Len(t, result, 1) 29 | }) 30 | t.Run("edge", func(t *testing.T) { 31 | ts := time.Date(2063, 4, 5, 0, 0, 0, 0, time.UTC) 32 | result := MakePartitionList(ts, ts.Add(partitionStep)) 33 | assert.Len(t, result, 2) 34 | assert.Equal(t, result[0].Date, "20630405") 35 | assert.Equal(t, result[0].Num, uint8(0)) 36 | }) 37 | t.Run("daily", func(t *testing.T) { 38 | ts := time.Now().Truncate(time.Hour * 24) 39 | result := MakePartitionList(ts, ts.Add(time.Hour*23)) 40 | assert.Len(t, result, int(numPartitions)) 41 | for i := uint8(0); i < uint8(numPartitions); i++ { 42 | assert.Equal(t, i, result[i].Num) 43 | } 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /schema/queries.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "fmt" 4 | 5 | var ( 6 | queryPartitions = "SELECT part_date, part_num, is_active FROM `%s`" 7 | queryActivePartitions = "SELECT part_date, part_num, is_active FROM `%s` WHERE is_active=true" 8 | deletePartitionQ = `DECLARE $part_date as Utf8; DECLARE $part_num as Uint8; 9 | DELETE FROM ` + "`%s`" + ` WHERE part_date = $part_date AND part_num = $part_num 10 | ` 11 | insertPartitionQ = `DECLARE $part_date as Utf8; 12 | DECLARE $part_num as Uint8; 13 | DECLARE $is_active as Bool; 14 | UPSERT INTO ` + "`%s`" + ` (part_date, part_num, is_active) VALUES ($part_date, $part_num, $is_active)` 15 | 16 | updatePartitionQ = `DECLARE $part_date as Utf8; 17 | DECLARE $part_num as Uint8; 18 | DECLARE $is_active as Bool; 19 | UPDATE ` + "`%s`" + ` SET is_active = $is_active WHERE part_date = $part_date AND part_num = $part_num` 20 | 21 | m = map[QueryName]queryInfo{ 22 | QueryParts: {"partitions", queryPartitions}, 23 | QueryActiveParts: {"partitions", queryActivePartitions}, 24 | DeletePart: {"partitions", deletePartitionQ}, 25 | InsertPart: {"partitions", insertPartitionQ}, 26 | UpdatePart: {"partitions", updatePartitionQ}, 27 | DeleteAllParts: {"partitions", "DELETE FROM `%s`"}, 28 | } 29 | ) 30 | 31 | type QueryName int 32 | 33 | const ( 34 | QueryParts QueryName = iota 35 | QueryActiveParts 36 | DeletePart 37 | InsertPart 38 | UpdatePart 39 | DeleteAllParts 40 | ) 41 | 42 | type queryInfo struct { 43 | table string 44 | query string 45 | } 46 | 47 | func BuildQuery(p DbPath, queryName QueryName) string { 48 | if i, ok := m[queryName]; ok { 49 | return fmt.Sprintf(i.query, p.Table(i.table)) 50 | } 51 | panic("query not found") 52 | } 53 | -------------------------------------------------------------------------------- /schema/schema.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "github.com/ydb-platform/ydb-go-sdk/v3/table/options" 6 | "github.com/ydb-platform/ydb-go-sdk/v3/table/types" 7 | 8 | "github.com/ydb-platform/jaeger-ydb-store/internal/db" 9 | ) 10 | 11 | // Definition is a list of create table options 12 | type ( 13 | Definition func() []options.CreateTableOption 14 | PartitionedDefinition func(partitionCount uint64) []options.CreateTableOption 15 | ) 16 | 17 | var ( 18 | // Tables are global tables 19 | Tables = map[string]Definition{ 20 | "partitions": Partitions, 21 | "service_names": ServiceNames, 22 | "operation_names_v2": OperationNamesV2, 23 | "archive": ArchiveTraces, 24 | } 25 | 26 | // PartitionTables tables split by partition 27 | PartitionTables = map[string]PartitionedDefinition{ 28 | "traces": Traces, 29 | "idx_service_name": ServiceNameIndex, 30 | "idx_service_op": ServiceOperationIndex, 31 | "idx_duration": DurationIndex, 32 | "idx_tag_v2": TagIndexV2, 33 | } 34 | ) 35 | 36 | // Traces returns traces table schema 37 | func Traces(numPartitions uint64) []options.CreateTableOption { 38 | return append( 39 | ArchiveTraces(), 40 | options.WithPartitions( 41 | options.WithUniformPartitions(numPartitions), 42 | ), 43 | options.WithPartitioningSettingsObject(partitioningSettings(numPartitions)), 44 | ) 45 | } 46 | 47 | // ArchiveTraces returns archive_traces table schema 48 | func ArchiveTraces() []options.CreateTableOption { 49 | res := []options.CreateTableOption{ 50 | options.WithColumn("trace_id_low", types.Optional(types.TypeUint64)), 51 | options.WithColumn("trace_id_high", types.Optional(types.TypeUint64)), 52 | options.WithColumn("span_id", types.Optional(types.TypeUint64)), 53 | options.WithColumn("operation_name", types.Optional(types.TypeUTF8)), 54 | options.WithColumn("flags", types.Optional(types.TypeUint32)), 55 | options.WithColumn("start_time", types.Optional(types.TypeInt64)), 56 | options.WithColumn("duration", types.Optional(types.TypeInt64)), 57 | options.WithColumn("extra", types.Optional(types.TypeString)), 58 | options.WithPrimaryKeyColumn("trace_id_low", "trace_id_high", "span_id"), 59 | } 60 | if viper.GetBool(db.KeyYDBFeatureCompression) { 61 | res = append(res, 62 | options.WithColumnFamilies( 63 | options.ColumnFamily{ 64 | Name: "default", 65 | Compression: options.ColumnFamilyCompressionLZ4, 66 | }, 67 | ), 68 | ) 69 | } 70 | return res 71 | } 72 | 73 | // ServiceOperationIndex returns service_operation_index table schema 74 | func ServiceOperationIndex(numPartitions uint64) []options.CreateTableOption { 75 | return []options.CreateTableOption{ 76 | options.WithColumn("idx_hash", types.Optional(types.TypeUint64)), 77 | options.WithColumn("rev_start_time", types.Optional(types.TypeInt64)), 78 | options.WithColumn("uniq", types.Optional(types.TypeUint32)), 79 | options.WithColumn("trace_ids", types.Optional(types.TypeString)), 80 | options.WithPrimaryKeyColumn("idx_hash", "rev_start_time", "uniq"), 81 | options.WithPartitions( 82 | options.WithUniformPartitions(numPartitions), 83 | ), 84 | options.WithPartitioningSettingsObject(partitioningSettings(numPartitions)), 85 | } 86 | } 87 | 88 | // ServiceNameIndex returns service_name_index table schema 89 | func ServiceNameIndex(numPartitions uint64) []options.CreateTableOption { 90 | return []options.CreateTableOption{ 91 | options.WithColumn("idx_hash", types.Optional(types.TypeUint64)), 92 | options.WithColumn("rev_start_time", types.Optional(types.TypeInt64)), 93 | options.WithColumn("uniq", types.Optional(types.TypeUint32)), 94 | options.WithColumn("trace_ids", types.Optional(types.TypeString)), 95 | options.WithPrimaryKeyColumn("idx_hash", "rev_start_time", "uniq"), 96 | options.WithPartitions( 97 | options.WithUniformPartitions(numPartitions), 98 | ), 99 | options.WithPartitioningSettingsObject(partitioningSettings(numPartitions)), 100 | } 101 | } 102 | 103 | // DurationIndex returns duration_index table schema 104 | func DurationIndex(numPartitions uint64) []options.CreateTableOption { 105 | return []options.CreateTableOption{ 106 | options.WithColumn("idx_hash", types.Optional(types.TypeUint64)), 107 | options.WithColumn("duration", types.Optional(types.TypeInt64)), 108 | options.WithColumn("rev_start_time", types.Optional(types.TypeInt64)), 109 | options.WithColumn("uniq", types.Optional(types.TypeUint32)), 110 | options.WithColumn("trace_ids", types.Optional(types.TypeString)), 111 | options.WithPrimaryKeyColumn("idx_hash", "duration", "rev_start_time", "uniq"), 112 | options.WithPartitions( 113 | options.WithUniformPartitions(numPartitions), 114 | ), 115 | options.WithPartitioningSettingsObject(partitioningSettings(numPartitions)), 116 | } 117 | } 118 | 119 | // TagIndexV2 returns tag_index_v2 table schema 120 | func TagIndexV2(numPartitions uint64) []options.CreateTableOption { 121 | return []options.CreateTableOption{ 122 | options.WithColumn("idx_hash", types.Optional(types.TypeUint64)), 123 | options.WithColumn("rev_start_time", types.Optional(types.TypeInt64)), 124 | options.WithColumn("op_hash", types.Optional(types.TypeUint64)), 125 | options.WithColumn("uniq", types.Optional(types.TypeUint32)), 126 | options.WithColumn("trace_ids", types.Optional(types.TypeString)), 127 | options.WithPrimaryKeyColumn("idx_hash", "rev_start_time", "op_hash", "uniq"), 128 | options.WithPartitions( 129 | options.WithUniformPartitions(numPartitions), 130 | ), 131 | options.WithPartitioningSettingsObject(partitioningSettings(numPartitions)), 132 | } 133 | } 134 | 135 | // ServiceNames returns service_names table schema 136 | func ServiceNames() []options.CreateTableOption { 137 | return []options.CreateTableOption{ 138 | options.WithColumn("service_name", types.Optional(types.TypeUTF8)), 139 | options.WithPrimaryKeyColumn("service_name"), 140 | } 141 | } 142 | 143 | // OperationNamesV2 returns operation_names_v2 table schema 144 | func OperationNamesV2() []options.CreateTableOption { 145 | return []options.CreateTableOption{ 146 | options.WithColumn("service_name", types.Optional(types.TypeUTF8)), 147 | options.WithColumn("span_kind", types.Optional(types.TypeUTF8)), 148 | options.WithColumn("operation_name", types.Optional(types.TypeUTF8)), 149 | options.WithPrimaryKeyColumn("service_name", "span_kind", "operation_name"), 150 | } 151 | } 152 | 153 | func Partitions() []options.CreateTableOption { 154 | return []options.CreateTableOption{ 155 | options.WithColumn("part_date", types.Optional(types.TypeUTF8)), 156 | options.WithColumn("part_num", types.Optional(types.TypeUint8)), 157 | options.WithColumn("is_active", types.Optional(types.TypeBool)), 158 | options.WithPrimaryKeyColumn("part_date", "part_num"), 159 | } 160 | } 161 | 162 | func partitioningSettings(numPartitions uint64) (settings options.PartitioningSettings) { 163 | settings = options.PartitioningSettings{ 164 | PartitioningBySize: options.FeatureEnabled, 165 | PartitionSizeMb: uint64(viper.GetSizeInBytes(db.KeyYDBPartitionSize) / 1024 / 1024), 166 | MinPartitionsCount: numPartitions, 167 | } 168 | if viper.GetBool(db.KeyYDBFeatureSplitByLoad) { 169 | settings.PartitioningByLoad = options.FeatureEnabled 170 | } 171 | return 172 | } 173 | -------------------------------------------------------------------------------- /storage/config/options.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ydb-platform/jaeger-ydb-store/schema" 7 | ) 8 | 9 | type Options struct { 10 | BufferSize int 11 | BatchSize int 12 | BatchWorkers int 13 | 14 | IndexerBufferSize int 15 | IndexerMaxTraces int 16 | IndexerMaxTTL time.Duration 17 | 18 | DbAddress string 19 | DbPath schema.DbPath 20 | 21 | PoolSize int 22 | QueryCacheSize int 23 | ConnectTimeout time.Duration 24 | WriteTimeout time.Duration 25 | RetryAttemptTimeout time.Duration 26 | WriteSvcOpCacheSize int // cache size for svc/operation index writer 27 | WriteMaxSpanAge time.Duration 28 | 29 | ReadTimeout time.Duration 30 | ReadQueryParallel int 31 | ReadOpLimit uint64 32 | ReadSvcLimit uint64 33 | } 34 | -------------------------------------------------------------------------------- /storage/dependencystore/storage.go: -------------------------------------------------------------------------------- 1 | package dependencystore 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/jaegertracing/jaeger/model" 8 | "google.golang.org/grpc/codes" 9 | "google.golang.org/grpc/status" 10 | ) 11 | 12 | // DependencyStore handles read/writes dependencies to YDB 13 | type DependencyStore struct{} 14 | 15 | // GetDependencies should return dependency data from YDB, but it's not stored there, so we return nothing 16 | func (DependencyStore) GetDependencies(ctx context.Context, endTs time.Time, lookback time.Duration) ([]model.DependencyLink, error) { 17 | return nil, status.Error(codes.Unimplemented, "not implemented") 18 | } 19 | -------------------------------------------------------------------------------- /storage/spanstore/batch/batch.go: -------------------------------------------------------------------------------- 1 | package batch 2 | 3 | type batch struct { 4 | items []interface{} 5 | } 6 | 7 | func newBatch(cnt int) *batch { 8 | return &batch{items: make([]interface{}, 0, cnt)} 9 | } 10 | 11 | func (b *batch) Append(item interface{}) { 12 | b.items = append(b.items, item) 13 | } 14 | 15 | func (b *batch) Len() int { 16 | return len(b.items) 17 | } 18 | -------------------------------------------------------------------------------- /storage/spanstore/batch/queue.go: -------------------------------------------------------------------------------- 1 | package batch 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/uber/jaeger-lib/metrics" 8 | ) 9 | 10 | const ( 11 | defaultBufferSize = 2000 12 | ) 13 | 14 | var ErrOverflow = errors.New("writer buffer overflow") 15 | 16 | // Queue represents queue of message batches 17 | type Queue struct { 18 | opts Options 19 | inFlight chan *batch 20 | itemBuffer chan interface{} 21 | writer Writer 22 | dropCounter metrics.Counter 23 | doneCh chan struct{} 24 | } 25 | 26 | type Writer interface { 27 | WriteItems(_ []interface{}) 28 | } 29 | 30 | type Options struct { 31 | BufferSize int 32 | BatchSize int 33 | BatchWorkers int 34 | } 35 | 36 | func NewQueue(opts Options, mf metrics.Factory, writer Writer) *Queue { 37 | if opts.BufferSize <= 0 { 38 | opts.BufferSize = defaultBufferSize 39 | } 40 | doneCh := make(chan struct{}) 41 | q := &Queue{ 42 | opts: opts, 43 | inFlight: make(chan *batch, 10), 44 | itemBuffer: make(chan interface{}, opts.BufferSize), 45 | writer: writer, 46 | dropCounter: mf.Counter(metrics.Options{Name: "dropped"}), 47 | doneCh: doneCh, 48 | } 49 | 50 | go q.inputProcessor() 51 | for i := 0; i < q.opts.BatchWorkers; i++ { 52 | go q.batchProcessor() 53 | } 54 | 55 | return q 56 | } 57 | 58 | func (w *Queue) Add(item interface{}) error { 59 | select { 60 | case w.itemBuffer <- item: 61 | return nil 62 | default: 63 | w.dropCounter.Inc(1) 64 | return ErrOverflow 65 | } 66 | } 67 | 68 | func (w *Queue) inputProcessor() { 69 | batch := newBatch(w.opts.BatchSize) 70 | flushTimer := time.NewTimer(time.Second) 71 | for { 72 | select { 73 | case <-w.doneCh: 74 | return 75 | case item := <-w.itemBuffer: 76 | batch.Append(item) 77 | if batch.Len() >= w.opts.BatchSize { 78 | w.inFlight <- batch 79 | batch = newBatch(w.opts.BatchSize) 80 | } 81 | case <-flushTimer.C: 82 | flushTimer.Reset(time.Second) 83 | if batch.Len() > 0 { 84 | w.inFlight <- batch 85 | batch = newBatch(w.opts.BatchSize) 86 | } 87 | } 88 | } 89 | } 90 | 91 | func (w *Queue) batchProcessor() { 92 | for { 93 | select { 94 | case <-w.doneCh: 95 | return 96 | case b := <-w.inFlight: 97 | w.writer.WriteItems(b.items) 98 | } 99 | } 100 | } 101 | 102 | func (w *Queue) Close() { 103 | w.doneCh <- struct{}{} 104 | } 105 | -------------------------------------------------------------------------------- /storage/spanstore/dbmodel/hash.go: -------------------------------------------------------------------------------- 1 | package dbmodel 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/dgryski/go-farm" 7 | ) 8 | 9 | func HashTagIndex(service, key, value string, bucket uint8) uint64 { 10 | return HashBucketData(bucket, service, key, value) 11 | } 12 | 13 | func HashBucketData(bucket uint8, lst ...string) uint64 { 14 | buf := new(bytes.Buffer) 15 | for _, s := range lst { 16 | buf.WriteString(s) 17 | } 18 | buf.WriteByte(bucket) 19 | return farm.Hash64(buf.Bytes()) 20 | } 21 | 22 | func HashData(lst ...string) uint64 { 23 | buf := new(bytes.Buffer) 24 | for _, s := range lst { 25 | buf.WriteString(s) 26 | } 27 | return farm.Hash64(buf.Bytes()) 28 | } 29 | -------------------------------------------------------------------------------- /storage/spanstore/dbmodel/index.go: -------------------------------------------------------------------------------- 1 | package dbmodel 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/jaegertracing/jaeger/model" 9 | ) 10 | 11 | const ( 12 | NumIndexBuckets = 10 13 | ) 14 | 15 | var ( 16 | errScanTraceID = errors.New("failed to scan TraceID") 17 | errListLength = errors.New("invalid length for TraceIDList") 18 | ) 19 | 20 | // TraceID represents db-serializable trace id 21 | type TraceID [16]byte 22 | 23 | func TraceIDFromDomain(src model.TraceID) TraceID { 24 | res := TraceID{} 25 | binary.BigEndian.PutUint64(res[:8], src.High) 26 | binary.BigEndian.PutUint64(res[8:], src.Low) 27 | return res 28 | } 29 | 30 | // Scan converts db result bytes slice to TraceID type 31 | func (dbTraceID *TraceID) Scan(src interface{}) error { 32 | switch v := src.(type) { 33 | case []byte: 34 | copy(dbTraceID[:], v[:16]) 35 | return nil 36 | default: 37 | return errScanTraceID 38 | } 39 | } 40 | 41 | // ToDomain converts trace ID from db-serializable form to domain TradeID 42 | func (dbTraceID TraceID) ToDomain() model.TraceID { 43 | traceIDHigh := binary.BigEndian.Uint64(dbTraceID[:8]) 44 | traceIDLow := binary.BigEndian.Uint64(dbTraceID[8:]) 45 | return model.NewTraceID(traceIDHigh, traceIDLow) 46 | } 47 | 48 | type TraceIDList []TraceID 49 | 50 | func (t *TraceIDList) Scan(src interface{}) error { 51 | var in []byte 52 | switch v := src.(type) { 53 | case []byte: 54 | in = v 55 | case string: 56 | in = []byte(v) 57 | default: 58 | return fmt.Errorf("invalid trace id list type: %T", src) 59 | } 60 | 61 | lst, err := TraceIDListFromBytes(in) 62 | if err != nil { 63 | return err 64 | } 65 | *t = lst 66 | return nil 67 | } 68 | 69 | func TraceIDListFromBytes(buf []byte) (TraceIDList, error) { 70 | if len(buf)%16 != 0 { 71 | return nil, errListLength 72 | } 73 | n := len(buf) / 16 74 | l := make(TraceIDList, n) 75 | tid := TraceID{} 76 | for i := 0; i < n; i++ { 77 | if err := tid.Scan(buf[i*16:]); err != nil { 78 | return nil, err 79 | } 80 | l[i] = tid 81 | } 82 | return l, nil 83 | } 84 | 85 | type IndexResult struct { 86 | Ids TraceIDList 87 | RevTs int64 88 | } 89 | -------------------------------------------------------------------------------- /storage/spanstore/dbmodel/model.go: -------------------------------------------------------------------------------- 1 | package dbmodel 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gogo/protobuf/proto" 8 | "github.com/jaegertracing/jaeger/model" 9 | ) 10 | 11 | // Span represents db-serializable model 12 | type Span struct { 13 | TraceIDLow uint64 14 | TraceIDHigh uint64 15 | SpanID uint64 16 | OperationName string 17 | Flags uint32 18 | StartTime int64 19 | Duration int64 20 | Extra []byte 21 | } 22 | 23 | // FromDomain converts plugin model to db model or returns error 24 | func FromDomain(span *model.Span) (*Span, error) { 25 | spanData := SpanData{ 26 | Process: span.Process, 27 | Tags: span.Tags, 28 | Logs: span.Logs, 29 | References: span.References, 30 | } 31 | extra, err := proto.Marshal(&spanData) 32 | if err != nil { 33 | return nil, fmt.Errorf("dbSpan.Extra marshal error: %w", err) 34 | } 35 | dbSpan := &Span{ 36 | TraceIDHigh: span.TraceID.High, 37 | TraceIDLow: span.TraceID.Low, 38 | SpanID: uint64(span.SpanID), 39 | OperationName: span.OperationName, 40 | Flags: uint32(span.Flags), 41 | StartTime: span.StartTime.UnixNano(), 42 | Duration: int64(span.Duration), 43 | Extra: extra, 44 | } 45 | return dbSpan, nil 46 | } 47 | 48 | // ToDomain converts db model to plugin model 49 | func ToDomain(dbSpan *Span) (*model.Span, error) { 50 | spanData := SpanData{} 51 | err := proto.Unmarshal(dbSpan.Extra, &spanData) 52 | if err != nil { 53 | return nil, fmt.Errorf("dbSpan.Extra unmarshal error: %w", err) 54 | } 55 | 56 | span := &model.Span{ 57 | TraceID: model.NewTraceID(dbSpan.TraceIDHigh, dbSpan.TraceIDLow), 58 | SpanID: model.SpanID(dbSpan.SpanID), 59 | OperationName: dbSpan.OperationName, 60 | Flags: model.Flags(dbSpan.Flags), 61 | StartTime: time.Unix(0, dbSpan.StartTime).UTC(), 62 | Duration: time.Duration(dbSpan.Duration), 63 | Process: spanData.Process, 64 | References: spanData.References, 65 | Tags: spanData.Tags, 66 | Logs: spanData.Logs, 67 | } 68 | return span, nil 69 | } 70 | -------------------------------------------------------------------------------- /storage/spanstore/dbmodel/model_test.go: -------------------------------------------------------------------------------- 1 | package dbmodel 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/jaegertracing/jaeger/model" 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/ydb-platform/jaeger-ydb-store/internal/testutil" 11 | ) 12 | 13 | func TestEncDec(t *testing.T) { 14 | span := &model.Span{ 15 | TraceID: testutil.GenerateTraceID(), 16 | SpanID: model.NewSpanID(42), 17 | StartTime: time.Now().Round(0).UTC(), 18 | Process: model.NewProcess("svc1", []model.KeyValue{ 19 | model.String("k", "v"), 20 | model.Int64("k2", 1), 21 | }), 22 | Tags: []model.KeyValue{ 23 | model.String("kk", "vv"), 24 | model.Int64("a", 1), 25 | }, 26 | References: []model.SpanRef{ 27 | {SpanID: 1, TraceID: model.NewTraceID(42, 0)}, 28 | }, 29 | Logs: []model.Log{ 30 | { 31 | Timestamp: time.Now().Round(0).UTC(), 32 | Fields: []model.KeyValue{model.String("log", "record")}, 33 | }, 34 | { 35 | Timestamp: time.Now().Round(0).UTC(), 36 | Fields: []model.KeyValue{model.String("log2", "record2")}, 37 | }, 38 | }, 39 | } 40 | 41 | dbSpan, err := FromDomain(span) 42 | if !assert.NoError(t, err) { 43 | return 44 | } 45 | resultSpan, err := ToDomain(dbSpan) 46 | if !assert.NoError(t, err) { 47 | return 48 | } 49 | assert.Equal(t, span.StartTime, resultSpan.StartTime) 50 | assert.Equal(t, span, resultSpan) 51 | } 52 | -------------------------------------------------------------------------------- /storage/spanstore/dbmodel/model_ydb.go: -------------------------------------------------------------------------------- 1 | package dbmodel 2 | 3 | import ( 4 | "github.com/ydb-platform/ydb-go-sdk/v3/table/types" 5 | ) 6 | 7 | func (s *Span) StructValue() types.Value { 8 | return types.StructValue( 9 | types.StructFieldValue("trace_id_low", types.OptionalValue(types.Uint64Value(s.TraceIDLow))), 10 | types.StructFieldValue("trace_id_high", types.OptionalValue(types.Uint64Value(s.TraceIDHigh))), 11 | types.StructFieldValue("span_id", types.OptionalValue(types.Uint64Value(s.SpanID))), 12 | types.StructFieldValue("operation_name", types.OptionalValue(types.TextValue(s.OperationName))), 13 | types.StructFieldValue("flags", types.OptionalValue(types.Uint32Value(s.Flags))), 14 | types.StructFieldValue("start_time", types.OptionalValue(types.Int64Value(s.StartTime))), 15 | types.StructFieldValue("duration", types.OptionalValue(types.Int64Value(s.Duration))), 16 | types.StructFieldValue("extra", types.OptionalValue(types.BytesValue(s.Extra))), 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /storage/spanstore/dbmodel/proto/model.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Uber Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax="proto3"; 16 | 17 | package jaeger.api_v2; 18 | 19 | import "gogoproto/gogo.proto"; 20 | import "google/api/annotations.proto"; 21 | import "google/protobuf/timestamp.proto"; 22 | import "google/protobuf/duration.proto"; 23 | 24 | // TODO: document all types and fields 25 | 26 | // TODO: once this moves to jaeger-idl repo, we may want to change Go pkg to api_v2 27 | // and rewrite it to model only in this repo. That should make it easier to generate 28 | // classes in other languages. 29 | option go_package = "model"; 30 | option java_package = "io.jaegertracing.api_v2"; 31 | 32 | // Enable gogoprotobuf extensions (https://github.com/gogo/protobuf/blob/master/extensions.md). 33 | // Enable custom Marshal method. 34 | option (gogoproto.marshaler_all) = true; 35 | // Enable custom Unmarshal method. 36 | option (gogoproto.unmarshaler_all) = true; 37 | // Enable custom Size method (Required by Marshal and Unmarshal). 38 | option (gogoproto.sizer_all) = true; 39 | // Enable registration with golang/protobuf for the grpc-gateway. 40 | option (gogoproto.goproto_registration) = true; 41 | 42 | enum ValueType { 43 | STRING = 0; 44 | BOOL = 1; 45 | INT64 = 2; 46 | FLOAT64 = 3; 47 | BINARY = 4; 48 | }; 49 | 50 | message KeyValue { 51 | option (gogoproto.equal) = true; 52 | option (gogoproto.compare) = true; 53 | 54 | string key = 1; 55 | ValueType v_type = 2; 56 | string v_str = 3; 57 | bool v_bool = 4; 58 | int64 v_int64 = 5; 59 | double v_float64 = 6; 60 | bytes v_binary = 7; 61 | } 62 | 63 | message Log { 64 | google.protobuf.Timestamp timestamp = 1 [ 65 | (gogoproto.stdtime) = true, 66 | (gogoproto.nullable) = false 67 | ]; 68 | repeated KeyValue fields = 2 [ 69 | (gogoproto.nullable) = false 70 | ]; 71 | } 72 | 73 | enum SpanRefType { 74 | CHILD_OF = 0; 75 | FOLLOWS_FROM = 1; 76 | }; 77 | 78 | message SpanRef { 79 | bytes trace_id = 1 [ 80 | (gogoproto.nullable) = false, 81 | (gogoproto.customtype) = "TraceID", 82 | (gogoproto.customname) = "TraceID" 83 | ]; 84 | bytes span_id = 2 [ 85 | (gogoproto.nullable) = false, 86 | (gogoproto.customtype) = "SpanID", 87 | (gogoproto.customname) = "SpanID" 88 | ]; 89 | SpanRefType ref_type = 3; 90 | } 91 | 92 | message Process { 93 | string service_name = 1; 94 | repeated KeyValue tags = 2 [ 95 | (gogoproto.nullable) = false 96 | ]; 97 | } 98 | 99 | message Span { 100 | bytes trace_id = 1 [ 101 | (gogoproto.nullable) = false, 102 | (gogoproto.customtype) = "TraceID", 103 | (gogoproto.customname) = "TraceID" 104 | ]; 105 | bytes span_id = 2 [ 106 | (gogoproto.nullable) = false, 107 | (gogoproto.customtype) = "SpanID", 108 | (gogoproto.customname) = "SpanID" 109 | ]; 110 | string operation_name = 3; 111 | repeated SpanRef references = 4 [ 112 | (gogoproto.nullable) = false 113 | ]; 114 | uint32 flags = 5 [ 115 | (gogoproto.nullable) = false, 116 | (gogoproto.customtype) = "Flags" 117 | ]; 118 | google.protobuf.Timestamp start_time = 6 [ 119 | (gogoproto.stdtime) = true, 120 | (gogoproto.nullable) = false 121 | ]; 122 | google.protobuf.Duration duration = 7 [ 123 | (gogoproto.stdduration) = true, 124 | (gogoproto.nullable) = false 125 | ]; 126 | repeated KeyValue tags = 8 [ 127 | (gogoproto.nullable) = false 128 | ]; 129 | repeated Log logs = 9 [ 130 | (gogoproto.nullable) = false 131 | ]; 132 | Process process = 10; 133 | string process_id = 11 [ 134 | (gogoproto.customname) = "ProcessID" 135 | ]; 136 | repeated string warnings = 12; 137 | } 138 | 139 | message Trace { 140 | message ProcessMapping { 141 | string process_id = 1 [ 142 | (gogoproto.customname) = "ProcessID" 143 | ]; 144 | Process process = 2 [ 145 | (gogoproto.nullable) = false 146 | ]; 147 | } 148 | repeated Span spans = 1; 149 | repeated ProcessMapping process_map = 2 [ 150 | (gogoproto.nullable) = false 151 | ]; 152 | repeated string warnings = 3; 153 | } 154 | 155 | message Batch { 156 | repeated Span spans = 1; 157 | Process process = 2 [ 158 | (gogoproto.nullable) = true 159 | ]; 160 | } 161 | 162 | message DependencyLink { 163 | string parent = 1; 164 | string child = 2; 165 | uint64 call_count = 3; 166 | string source = 4; 167 | } 168 | -------------------------------------------------------------------------------- /storage/spanstore/dbmodel/proto/spandata.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option go_package = "dbmodel"; 3 | 4 | import "model.proto"; 5 | import "gogoproto/gogo.proto"; 6 | 7 | 8 | message SpanData { 9 | jaeger.api_v2.Process Process = 1; 10 | repeated jaeger.api_v2.KeyValue tags = 2 [(gogoproto.nullable) = false]; 11 | repeated jaeger.api_v2.Log logs = 3 [(gogoproto.nullable) = false]; 12 | repeated jaeger.api_v2.SpanRef references = 4 [(gogoproto.nullable) = false]; 13 | } -------------------------------------------------------------------------------- /storage/spanstore/dbmodel/unique_ids.go: -------------------------------------------------------------------------------- 1 | package dbmodel 2 | 3 | type UniqueTraceIDs struct { 4 | m map[TraceID]struct{} 5 | l []TraceID 6 | } 7 | 8 | func NewUniqueTraceIDs() *UniqueTraceIDs { 9 | return &UniqueTraceIDs{ 10 | m: make(map[TraceID]struct{}), 11 | l: make([]TraceID, 0), 12 | } 13 | } 14 | 15 | func (m *UniqueTraceIDs) Add(id TraceID) { 16 | if _, contains := m.m[id]; !contains { 17 | m.m[id] = struct{}{} 18 | m.l = append(m.l, id) 19 | } 20 | } 21 | 22 | func (m *UniqueTraceIDs) Has(id TraceID) bool { 23 | _, ok := m.m[id] 24 | return ok 25 | } 26 | 27 | func (m *UniqueTraceIDs) Len() int { 28 | return len(m.m) 29 | } 30 | 31 | func (m *UniqueTraceIDs) AsList() []TraceID { 32 | return m.l 33 | } 34 | 35 | func (m *UniqueTraceIDs) JoinWith(b *UniqueTraceIDs) { 36 | for id := range b.m { 37 | m.Add(id) 38 | } 39 | } 40 | 41 | // IntersectTraceIDs takes a list of UniqueTraceIDs and intersects them. 42 | func IntersectTraceIDs(uniqueTraceIdsList []*UniqueTraceIDs) *UniqueTraceIDs { 43 | retMe := NewUniqueTraceIDs() 44 | for key := range uniqueTraceIdsList[0].m { 45 | keyExistsInAll := true 46 | for _, otherTraceIds := range uniqueTraceIdsList[1:] { 47 | if !otherTraceIds.Has(key) { 48 | keyExistsInAll = false 49 | break 50 | } 51 | } 52 | if keyExistsInAll { 53 | retMe.Add(key) 54 | } 55 | } 56 | return retMe 57 | } 58 | -------------------------------------------------------------------------------- /storage/spanstore/dbmodel/unique_ids_test.go: -------------------------------------------------------------------------------- 1 | package dbmodel 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/jaegertracing/jaeger/model" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func sortTraceIDs(traceIDs []TraceID) []TraceID { 13 | sort.Slice(traceIDs, func(i, j int) bool { 14 | return strings.Compare(string(traceIDs[i][:]), string(traceIDs[j][:])) < 0 15 | }) 16 | return traceIDs 17 | } 18 | 19 | func TestIntersectTraceIDs(t *testing.T) { 20 | a := NewUniqueTraceIDs() 21 | a.Add(TraceIDFromDomain(model.NewTraceID(1, 1))) 22 | a.Add(TraceIDFromDomain(model.NewTraceID(1, 2))) 23 | a.Add(TraceIDFromDomain(model.NewTraceID(1, 3))) 24 | b := NewUniqueTraceIDs() 25 | b.Add(TraceIDFromDomain(model.NewTraceID(1, 2))) 26 | b.Add(TraceIDFromDomain(model.NewTraceID(1, 3))) 27 | b.Add(TraceIDFromDomain(model.NewTraceID(1, 4))) 28 | 29 | result := sortTraceIDs(IntersectTraceIDs([]*UniqueTraceIDs{a, b}).AsList()) 30 | expected := sortTraceIDs([]TraceID{TraceIDFromDomain(model.NewTraceID(1, 2)), TraceIDFromDomain(model.NewTraceID(1, 3))}) 31 | assert.Equal(t, expected, result) 32 | } 33 | -------------------------------------------------------------------------------- /storage/spanstore/indexer/bucket.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | var r = newLockedRand(time.Now().UnixNano()) 8 | 9 | type bucketRR struct { 10 | max uint8 11 | cur uint8 12 | } 13 | 14 | func newBucketRR(max uint8) *bucketRR { 15 | return &bucketRR{ 16 | max: max, 17 | cur: uint8(r.Intn(int(max))), 18 | } 19 | } 20 | 21 | func (b *bucketRR) Next() uint8 { 22 | v := b.cur 23 | b.cur++ 24 | if b.cur == b.max { 25 | b.cur = 0 26 | } 27 | return v 28 | } 29 | -------------------------------------------------------------------------------- /storage/spanstore/indexer/bucket_test.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestBucket(t *testing.T) { 10 | b := bucketRR{max: 5} 11 | res := make([]uint8, 0) 12 | for i := 0; i < 11; i++ { 13 | res = append(res, b.Next()) 14 | } 15 | exp := []uint8{0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0} 16 | assert.Equal(t, exp, res) 17 | } 18 | -------------------------------------------------------------------------------- /storage/spanstore/indexer/index/base.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jaegertracing/jaeger/model" 7 | "github.com/ydb-platform/ydb-go-sdk/v3/table/types" 8 | ) 9 | 10 | type Indexable interface { 11 | Hash() uint64 12 | StructFields(bucket uint8) []types.StructValueOption 13 | Timestamp() time.Time 14 | } 15 | 16 | type baseIndex struct { 17 | startTime time.Time 18 | } 19 | 20 | func newBaseIndex(span *model.Span) baseIndex { 21 | return baseIndex{ 22 | startTime: span.StartTime, 23 | } 24 | } 25 | 26 | func (i baseIndex) Timestamp() time.Time { 27 | return i.startTime 28 | } 29 | -------------------------------------------------------------------------------- /storage/spanstore/indexer/index/idx_duration.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "time" 7 | 8 | "github.com/dgryski/go-farm" 9 | "github.com/jaegertracing/jaeger/model" 10 | "github.com/ydb-platform/ydb-go-sdk/v3/table/types" 11 | 12 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/dbmodel" 13 | ) 14 | 15 | func DurationIndexValue(d time.Duration) int64 { 16 | switch { 17 | case d < time.Millisecond*100: 18 | return int64(d.Truncate(time.Millisecond * 10)) 19 | case d < time.Second: 20 | return int64(d.Truncate(time.Millisecond * 100)) 21 | default: 22 | return int64(d.Truncate(time.Second / 2)) 23 | } 24 | } 25 | 26 | type durationIndex struct { 27 | baseIndex 28 | serviceName string 29 | operationName string 30 | duration int64 31 | } 32 | 33 | func NewDurationIndex(span *model.Span, opName string) Indexable { 34 | return durationIndex{ 35 | baseIndex: newBaseIndex(span), 36 | serviceName: span.Process.ServiceName, 37 | operationName: opName, 38 | duration: DurationIndexValue(span.Duration), 39 | } 40 | } 41 | 42 | func (i durationIndex) Hash() uint64 { 43 | buf := new(bytes.Buffer) 44 | buf.WriteString(i.serviceName) 45 | buf.WriteString(i.operationName) 46 | _ = binary.Write(buf, binary.BigEndian, i.duration) 47 | return farm.Hash64(buf.Bytes()) 48 | } 49 | 50 | func (i durationIndex) StructFields(bucket uint8) []types.StructValueOption { 51 | return []types.StructValueOption{ 52 | types.StructFieldValue("idx_hash", types.Uint64Value(dbmodel.HashBucketData(bucket, i.serviceName, i.operationName))), 53 | types.StructFieldValue("duration", types.Int64Value(i.duration)), 54 | types.StructFieldValue("rev_start_time", types.Int64Value(-i.startTime.UnixNano())), 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /storage/spanstore/indexer/index/idx_operation.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "github.com/jaegertracing/jaeger/model" 5 | "github.com/ydb-platform/ydb-go-sdk/v3/table/types" 6 | 7 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/dbmodel" 8 | ) 9 | 10 | type serviceOperationIndex struct { 11 | baseIndex 12 | serviceName string 13 | operationName string 14 | } 15 | 16 | func NewServiceOperationIndex(span *model.Span) Indexable { 17 | return serviceOperationIndex{ 18 | baseIndex: newBaseIndex(span), 19 | serviceName: span.Process.ServiceName, 20 | operationName: span.OperationName, 21 | } 22 | } 23 | 24 | func (s serviceOperationIndex) Hash() uint64 { 25 | return dbmodel.HashData(s.serviceName, s.operationName) 26 | } 27 | 28 | func (s serviceOperationIndex) StructFields(bucket uint8) []types.StructValueOption { 29 | return []types.StructValueOption{ 30 | types.StructFieldValue("idx_hash", types.Uint64Value(s.Hash())), 31 | types.StructFieldValue("rev_start_time", types.Int64Value(-s.startTime.UnixNano())), 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /storage/spanstore/indexer/index/idx_service.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "github.com/jaegertracing/jaeger/model" 5 | "github.com/ydb-platform/ydb-go-sdk/v3/table/types" 6 | 7 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/dbmodel" 8 | ) 9 | 10 | type serviceNameIndex struct { 11 | baseIndex 12 | serviceName string 13 | } 14 | 15 | func NewServiceNameIndex(span *model.Span) Indexable { 16 | return serviceNameIndex{ 17 | baseIndex: newBaseIndex(span), 18 | serviceName: span.Process.ServiceName, 19 | } 20 | } 21 | 22 | func (s serviceNameIndex) Hash() uint64 { 23 | return dbmodel.HashData(s.serviceName) 24 | } 25 | 26 | func (s serviceNameIndex) StructFields(bucket uint8) []types.StructValueOption { 27 | return []types.StructValueOption{ 28 | types.StructFieldValue("idx_hash", types.Uint64Value(dbmodel.HashBucketData(bucket, s.serviceName))), 29 | types.StructFieldValue("rev_start_time", types.Int64Value(-s.startTime.UnixNano())), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /storage/spanstore/indexer/index/idx_tag.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "github.com/jaegertracing/jaeger/model" 5 | "github.com/ydb-platform/ydb-go-sdk/v3/table/types" 6 | 7 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/dbmodel" 8 | ) 9 | 10 | type tagIndex struct { 11 | baseIndex 12 | serviceName string 13 | opName string 14 | key string 15 | value string 16 | } 17 | 18 | func NewTagIndex(span *model.Span, kv model.KeyValue) Indexable { 19 | return tagIndex{ 20 | baseIndex: newBaseIndex(span), 21 | serviceName: span.GetProcess().GetServiceName(), 22 | opName: span.GetOperationName(), 23 | key: kv.Key, 24 | value: kv.AsString(), 25 | } 26 | } 27 | 28 | func (t tagIndex) Hash() uint64 { 29 | return dbmodel.HashData(t.serviceName, t.opName, t.key, t.value) 30 | } 31 | 32 | func (t tagIndex) StructFields(bucket uint8) []types.StructValueOption { 33 | return []types.StructValueOption{ 34 | types.StructFieldValue("idx_hash", types.Uint64Value(dbmodel.HashTagIndex(t.serviceName, t.key, t.value, bucket))), 35 | types.StructFieldValue("rev_start_time", types.Int64Value(-t.startTime.UnixNano())), 36 | types.StructFieldValue("op_hash", types.Uint64Value(dbmodel.HashData(t.opName))), 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /storage/spanstore/indexer/index/trace_ids.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/jaegertracing/jaeger/model" 7 | ) 8 | 9 | type TraceIDList []model.TraceID 10 | 11 | func (l TraceIDList) ToBytes() []byte { 12 | buf := make([]byte, 16*len(l)) 13 | var err error 14 | for i, id := range l { 15 | _, err = id.MarshalTo(buf[i*16:]) 16 | if err != nil { 17 | panic(err) 18 | } 19 | } 20 | return buf 21 | } 22 | 23 | func TraceIDListFromBytes(b []byte) (TraceIDList, error) { 24 | if len(b)%16 != 0 { 25 | return nil, errors.New("trace id unmarshal err: invalid length") 26 | } 27 | n := len(b) / 16 28 | l := make(TraceIDList, n) 29 | id := model.TraceID{} 30 | for i := 0; i < n; i++ { 31 | if err := id.Unmarshal(b[:16]); err != nil { 32 | return nil, err 33 | } 34 | l[i] = id 35 | b = b[16:] 36 | } 37 | return l, nil 38 | } 39 | -------------------------------------------------------------------------------- /storage/spanstore/indexer/index/trace_ids_test.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/jaegertracing/jaeger/model" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestTraceIDList_ToBytes(t *testing.T) { 11 | l := TraceIDList{ 12 | model.NewTraceID(1, 2), 13 | model.NewTraceID(2, 3), 14 | model.NewTraceID(4, 5), 15 | } 16 | result := l.ToBytes() 17 | assert.Len(t, result, 48) 18 | 19 | resultList, err := TraceIDListFromBytes(result) 20 | if !assert.NoError(t, err) { 21 | return 22 | } 23 | assert.Equal(t, resultList, l) 24 | } 25 | -------------------------------------------------------------------------------- /storage/spanstore/indexer/indexer.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/hashicorp/go-hclog" 7 | "github.com/jaegertracing/jaeger/model" 8 | "github.com/uber/jaeger-lib/metrics" 9 | "github.com/ydb-platform/ydb-go-sdk/v3/table" 10 | "go.uber.org/zap" 11 | 12 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/indexer/index" 13 | ) 14 | 15 | const ( 16 | tblTagIndex = "idx_tag_v2" 17 | tblDurationIndex = "idx_duration" 18 | tblServiceNameIndex = "idx_service_name" 19 | tblServiceOperationIndex = "idx_service_op" 20 | ) 21 | 22 | var ErrOverflow = errors.New("indexer buffer overflow") 23 | 24 | type Indexer struct { 25 | opts Options 26 | logger *zap.Logger 27 | jaegerLogger hclog.Logger 28 | 29 | inputItems chan *model.Span 30 | tagWriter *indexWriter 31 | svcWriter *indexWriter 32 | opWriter *indexWriter 33 | durationWriter *indexWriter 34 | dropCounter metrics.Counter 35 | doneCh chan struct{} 36 | } 37 | 38 | func NewIndexer(pool table.Client, mf metrics.Factory, logger *zap.Logger, jaegerLogger hclog.Logger, opts Options) *Indexer { 39 | doneCh := make(chan struct{}) 40 | indexer := &Indexer{ 41 | logger: logger, 42 | jaegerLogger: jaegerLogger, 43 | opts: opts, 44 | 45 | inputItems: make(chan *model.Span, opts.BufferSize), 46 | dropCounter: mf.Counter(metrics.Options{Name: "indexer_dropped"}), 47 | doneCh: doneCh, 48 | } 49 | indexer.tagWriter = newIndexWriter(pool, mf.Namespace(metrics.NSOptions{Name: "tag_index"}), logger, jaegerLogger, tblTagIndex, opts) 50 | indexer.svcWriter = newIndexWriter(pool, mf.Namespace(metrics.NSOptions{Name: "service_name_index"}), logger, jaegerLogger, tblServiceNameIndex, opts) 51 | indexer.opWriter = newIndexWriter(pool, mf.Namespace(metrics.NSOptions{Name: "service_operation_index"}), logger, jaegerLogger, tblServiceOperationIndex, opts) 52 | indexer.durationWriter = newIndexWriter(pool, mf.Namespace(metrics.NSOptions{Name: "duration_index"}), logger, jaegerLogger, tblDurationIndex, opts) 53 | 54 | go indexer.spanProcessor() 55 | 56 | return indexer 57 | } 58 | 59 | func (w *Indexer) Add(span *model.Span) error { 60 | select { 61 | case w.inputItems <- span: 62 | return nil 63 | default: 64 | w.dropCounter.Inc(1) 65 | return ErrOverflow 66 | } 67 | } 68 | 69 | func (w *Indexer) spanProcessor() { 70 | for { 71 | select { 72 | case <-w.doneCh: 73 | return 74 | case span := <-w.inputItems: 75 | for _, tag := range span.GetTags() { 76 | w.processTag(tag, span) 77 | } 78 | if spanProcess := span.GetProcess(); spanProcess != nil { 79 | for _, tag := range spanProcess.GetTags() { 80 | w.processTag(tag, span) 81 | } 82 | } 83 | w.svcWriter.Add(index.NewServiceNameIndex(span), span.TraceID) 84 | w.opWriter.Add(index.NewServiceOperationIndex(span), span.TraceID) 85 | if span.OperationName != "" { 86 | w.durationWriter.Add(index.NewDurationIndex(span, span.OperationName), span.TraceID) 87 | } 88 | w.durationWriter.Add(index.NewDurationIndex(span, ""), span.TraceID) 89 | } 90 | } 91 | } 92 | 93 | func (w *Indexer) processTag(kv model.KeyValue, span *model.Span) { 94 | if shouldIndexTag(kv) { 95 | w.tagWriter.Add(index.NewTagIndex(span, kv), span.TraceID) 96 | } 97 | } 98 | 99 | func (w *Indexer) Close() { 100 | w.doneCh <- struct{}{} 101 | } 102 | -------------------------------------------------------------------------------- /storage/spanstore/indexer/options.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ydb-platform/jaeger-ydb-store/schema" 7 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/batch" 8 | ) 9 | 10 | type Options struct { 11 | DbPath schema.DbPath 12 | MaxTraces int 13 | MaxTTL time.Duration 14 | BufferSize int 15 | Batch batch.Options 16 | WriteTimeout time.Duration 17 | RetryAttemptTimeout time.Duration 18 | } 19 | -------------------------------------------------------------------------------- /storage/spanstore/indexer/rand.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | ) 7 | 8 | type lockedSource struct { 9 | lk sync.Mutex 10 | src rand.Source 11 | } 12 | 13 | func newLockedRand(seed int64) *rand.Rand { 14 | return rand.New(&lockedSource{src: rand.NewSource(seed)}) 15 | } 16 | 17 | func (l *lockedSource) Int63() int64 { 18 | l.lk.Lock() 19 | defer l.lk.Unlock() 20 | return l.src.Int63() 21 | } 22 | 23 | func (l *lockedSource) Seed(seed int64) { 24 | l.lk.Lock() 25 | defer l.lk.Unlock() 26 | l.src.Seed(seed) 27 | } 28 | -------------------------------------------------------------------------------- /storage/spanstore/indexer/tag_helper.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import "github.com/jaegertracing/jaeger/model" 4 | 5 | var ( 6 | stopList = []string{"sampler.type", "sampler.param", "internal.span.format"} 7 | stopMap map[string]struct{} 8 | ) 9 | 10 | func init() { 11 | stopMap = make(map[string]struct{}, len(stopList)) 12 | for _, l := range stopList { 13 | stopMap[l] = struct{}{} 14 | } 15 | } 16 | 17 | func shouldIndexTag(kv model.KeyValue) bool { 18 | if kv.VType == model.ValueType_BINARY { 19 | return false 20 | } 21 | if _, exists := stopMap[kv.Key]; exists { 22 | return false 23 | } 24 | return true 25 | } 26 | -------------------------------------------------------------------------------- /storage/spanstore/indexer/ttl_map.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/jaegertracing/jaeger/model" 8 | 9 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/indexer/index" 10 | ) 11 | 12 | type indexTTLMap struct { 13 | maxItemsPerKey int 14 | maxTTL time.Duration 15 | 16 | evict indexMapEvictFunc 17 | m map[indexMapKey]*ttlMapValue 18 | l sync.Mutex 19 | } 20 | 21 | type indexMapKey struct { 22 | hash uint64 23 | ts int64 24 | } 25 | 26 | type ttlMapValue struct { 27 | idx index.Indexable 28 | traceIds []model.TraceID 29 | lastAccess time.Time 30 | } 31 | 32 | type indexMapEvictFunc func(idx index.Indexable, traceIds []model.TraceID) 33 | 34 | func newIndexMap(evict indexMapEvictFunc, maxItemsPerKey int, maxTTL time.Duration) *indexTTLMap { 35 | if maxItemsPerKey <= 0 { 36 | panic("maxItemsPerKey invalid value") 37 | } 38 | if maxTTL <= 0 { 39 | panic("maxTTLSeconds invalid value") 40 | } 41 | m := &indexTTLMap{ 42 | maxItemsPerKey: maxItemsPerKey, 43 | maxTTL: maxTTL, 44 | evict: evict, 45 | m: make(map[indexMapKey]*ttlMapValue), 46 | } 47 | go m.evictProcess() 48 | return m 49 | } 50 | 51 | func (m *indexTTLMap) evictProcess() { 52 | for now := range time.Tick(time.Second) { 53 | m.l.Lock() 54 | for k, v := range m.m { 55 | if now.Sub(v.lastAccess) >= m.maxTTL { 56 | delete(m.m, k) 57 | m.evict(v.idx, v.traceIds) 58 | } 59 | } 60 | m.l.Unlock() 61 | } 62 | } 63 | 64 | func (m *indexTTLMap) Add(idx index.Indexable, traceId model.TraceID) { 65 | m.l.Lock() 66 | defer m.l.Unlock() 67 | key := indexMapKey{ 68 | hash: idx.Hash(), 69 | ts: idx.Timestamp().Truncate(time.Second * 5).Unix(), 70 | } 71 | var v *ttlMapValue 72 | if vv, ok := m.m[key]; !ok { 73 | v = &ttlMapValue{ 74 | idx: idx, 75 | traceIds: make([]model.TraceID, 0, m.maxItemsPerKey), 76 | } 77 | m.m[key] = v 78 | } else { 79 | v = vv 80 | } 81 | v.traceIds = append(m.m[key].traceIds, traceId) 82 | v.lastAccess = time.Now() 83 | if len(v.traceIds) >= m.maxItemsPerKey { 84 | delete(m.m, key) 85 | m.evict(v.idx, v.traceIds) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /storage/spanstore/indexer/writer.go: -------------------------------------------------------------------------------- 1 | package indexer 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/hashicorp/go-hclog" 9 | "github.com/jaegertracing/jaeger/model" 10 | "github.com/uber/jaeger-lib/metrics" 11 | "github.com/ydb-platform/ydb-go-sdk/v3/table" 12 | "github.com/ydb-platform/ydb-go-sdk/v3/table/types" 13 | "go.uber.org/zap" 14 | 15 | "github.com/ydb-platform/jaeger-ydb-store/internal/db" 16 | "github.com/ydb-platform/jaeger-ydb-store/schema" 17 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/batch" 18 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/dbmodel" 19 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/indexer/index" 20 | wmetrics "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/writer/metrics" 21 | ) 22 | 23 | type indexWriter struct { 24 | pool table.Client 25 | logger *zap.Logger 26 | jaegerLogger hclog.Logger 27 | metrics indexerMetrics 28 | tableName string 29 | opts Options 30 | 31 | idxRand *rand.Rand 32 | batch *batch.Queue 33 | *indexTTLMap 34 | } 35 | 36 | type indexData struct { 37 | idx index.Indexable 38 | traceIds index.TraceIDList 39 | } 40 | 41 | type indexerMetrics interface { 42 | Emit(err error, latency time.Duration, count int) 43 | } 44 | 45 | func newIndexWriter(pool table.Client, mf metrics.Factory, logger *zap.Logger, jaegerLogger hclog.Logger, tableName string, opts Options) *indexWriter { 46 | w := &indexWriter{ 47 | pool: pool, 48 | logger: logger, 49 | jaegerLogger: jaegerLogger, 50 | metrics: wmetrics.NewWriteMetrics(mf, ""), 51 | tableName: tableName, 52 | opts: opts, 53 | idxRand: newLockedRand(time.Now().UnixNano()), 54 | } 55 | w.indexTTLMap = newIndexMap(w.flush, opts.MaxTraces, opts.MaxTTL) 56 | w.batch = batch.NewQueue(opts.Batch, mf, w) 57 | return w 58 | } 59 | 60 | func (w *indexWriter) flush(idx index.Indexable, traceIds []model.TraceID) { 61 | err := w.batch.Add(indexData{ 62 | idx: idx, 63 | traceIds: traceIds, 64 | }) 65 | switch { 66 | case err == batch.ErrOverflow: 67 | case err != nil: 68 | w.logger.Error("indexer batch error", zap.String("table", w.tableName), zap.Error(err)) 69 | w.jaegerLogger.Error( 70 | "indexer batch error", 71 | "table", w.tableName, 72 | "error", err, 73 | ) 74 | } 75 | } 76 | 77 | func (w *indexWriter) WriteItems(items []interface{}) { 78 | parts := map[schema.PartitionKey][]indexData{} 79 | for _, item := range items { 80 | data := item.(indexData) 81 | k := schema.PartitionFromTime(data.idx.Timestamp()) 82 | parts[k] = append(parts[k], data) 83 | } 84 | for k, partial := range parts { 85 | w.writePartition(k, partial) 86 | } 87 | } 88 | 89 | func (w *indexWriter) writePartition(part schema.PartitionKey, items []indexData) { 90 | fullTableName := tableName(w.opts.DbPath, part, w.tableName) 91 | brr := newBucketRR(dbmodel.NumIndexBuckets) 92 | rows := make([]types.Value, 0, len(items)) 93 | for _, item := range items { 94 | brr.Next() 95 | // nolint: typecheck, nolintlint 96 | buf := item.traceIds.ToBytes() 97 | fields := item.idx.StructFields(brr.Next()) 98 | fields = append(fields, 99 | types.StructFieldValue("uniq", types.Uint32Value(w.idxRand.Uint32())), 100 | types.StructFieldValue("trace_ids", types.BytesValue(buf)), 101 | ) 102 | rows = append(rows, types.StructValue(fields...)) 103 | } 104 | ts := time.Now() 105 | 106 | ctx := context.Background() 107 | if w.opts.WriteTimeout > 0 { 108 | var cancel context.CancelFunc 109 | ctx, cancel = context.WithTimeout(ctx, w.opts.WriteTimeout) 110 | defer cancel() 111 | } 112 | err := db.UpsertData(ctx, w.pool, fullTableName, types.ListValue(rows...), w.opts.RetryAttemptTimeout) 113 | 114 | w.metrics.Emit(err, time.Since(ts), len(rows)) 115 | if err != nil { 116 | w.logger.Error("indexer write fail", zap.String("table", w.tableName), zap.Error(err)) 117 | w.jaegerLogger.Error( 118 | "indexer write fail", 119 | "table", w.tableName, 120 | "error", err, 121 | ) 122 | } 123 | } 124 | 125 | func tableName(dbPath schema.DbPath, part schema.PartitionKey, tableName string) string { 126 | return part.BuildFullTableName(dbPath.String(), tableName) 127 | } 128 | -------------------------------------------------------------------------------- /storage/spanstore/queries/reader_queries.go: -------------------------------------------------------------------------------- 1 | package queries 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/ydb-platform/jaeger-ydb-store/schema" 8 | ) 9 | 10 | const ( 11 | queryByTraceID = `DECLARE $trace_id_high AS uint64; 12 | DECLARE $trace_id_low AS uint64; 13 | DECLARE $limit AS uint64; 14 | DECLARE $offset as uint64; 15 | SELECT trace_id_high, trace_id_low, span_id, operation_name, flags, start_time, duration, extra 16 | FROM ` + "`%s`" + ` 17 | WHERE trace_id_high = $trace_id_high and trace_id_low = $trace_id_low 18 | LIMIT $offset,$limit` 19 | 20 | querySpanCount = `DECLARE $trace_id_high AS uint64; 21 | DECLARE $trace_id_low AS uint64; 22 | SELECT COUNT(*) AS c 23 | FROM ` + "`%s`" + ` 24 | WHERE trace_id_high = $trace_id_high AND trace_id_low = $trace_id_low` 25 | 26 | queryByTag = `DECLARE $hash AS uint64; 27 | DECLARE $time_min AS int64; 28 | DECLARE $time_max AS int64; 29 | DECLARE $limit AS uint64; 30 | SELECT trace_ids, rev_start_time 31 | FROM ` + "`%s`" + ` 32 | WHERE idx_hash = $hash AND rev_start_time <= 0-$time_min AND rev_start_time >= 0-$time_max 33 | LIMIT $limit` 34 | 35 | queryByTagAndOperation = `DECLARE $hash AS uint64; 36 | DECLARE $op_hash as uint64; 37 | DECLARE $time_min AS int64; 38 | DECLARE $time_max AS int64; 39 | DECLARE $limit AS uint64; 40 | SELECT trace_ids, rev_start_time 41 | FROM ` + "`%s`" + ` 42 | WHERE idx_hash = $hash AND rev_start_time <= 0-$time_min AND rev_start_time >= 0-$time_max 43 | AND op_hash = $op_hash 44 | LIMIT $limit` 45 | 46 | queryByServiceAndOperationName = `DECLARE $hash AS uint64; 47 | DECLARE $time_min AS int64; 48 | DECLARE $time_max AS int64; 49 | DECLARE $limit AS uint64; 50 | SELECT trace_ids, rev_start_time 51 | FROM ` + "`%s`" + ` 52 | WHERE idx_hash = $hash AND rev_start_time <= 0-$time_min AND rev_start_time >= 0-$time_max 53 | LIMIT $limit` 54 | 55 | queryByDuration = `DECLARE $hash AS uint64; 56 | DECLARE $duration_min AS int64; 57 | DECLARE $duration_max AS int64; 58 | DECLARE $time_min AS int64; 59 | DECLARE $time_max AS int64; 60 | DECLARE $limit AS uint64; 61 | SELECT trace_ids, rev_start_time 62 | FROM ` + "`%s`" + ` 63 | WHERE idx_hash = $hash AND rev_start_time <= 0-$time_min AND rev_start_time >= 0-$time_max 64 | AND duration >= $duration_min AND duration <= $duration_max 65 | LIMIT $limit` 66 | 67 | queryByServiceName = `DECLARE $hash AS uint64; 68 | DECLARE $time_min AS int64; 69 | DECLARE $time_max AS int64; 70 | DECLARE $limit AS uint64; 71 | SELECT trace_ids, rev_start_time 72 | FROM ` + "`%s`" + ` 73 | WHERE idx_hash = $hash AND rev_start_time <= 0-$time_min AND rev_start_time >= 0-$time_max 74 | LIMIT $limit` 75 | 76 | queryServiceNames = `DECLARE $limit AS uint64; 77 | SELECT service_name 78 | FROM ` + "`%s`" + ` 79 | LIMIT $limit` 80 | 81 | queryOperations = `DECLARE $service_name AS utf8; 82 | DECLARE $limit AS uint64; 83 | SELECT operation_name 84 | FROM ` + "`%s`" + ` 85 | WHERE service_name = $service_name 86 | LIMIT $limit` 87 | 88 | queryOperationsWithKind = `DECLARE $service_name AS utf8; 89 | DECLARE $span_kind AS utf8; 90 | DECLARE $limit AS uint64; 91 | SELECT operation_name 92 | FROM ` + "`%s`" + ` 93 | WHERE service_name = $service_name AND span_kind = $span_kind 94 | LIMIT $limit` 95 | ) 96 | 97 | var ( 98 | m = map[string]queryInfo{ 99 | "query-services": {"service_names", queryServiceNames}, 100 | "query-operations": {"operation_names_v2", queryOperations}, 101 | "query-operations-with-kind": {"operation_names_v2", queryOperationsWithKind}, 102 | "queryByTraceID": {"archive", queryByTraceID}, 103 | "querySpanCount": {"archive", querySpanCount}, 104 | } 105 | 106 | pm = map[string]queryInfo{ 107 | "queryByTraceID": {"traces", queryByTraceID}, 108 | "querySpanCount": {"traces", querySpanCount}, 109 | "queryByTag": {"idx_tag_v2", queryByTag}, 110 | "queryByTagAndOperation": {"idx_tag_v2", queryByTagAndOperation}, 111 | "queryByDuration": {"idx_duration", queryByDuration}, 112 | "queryByServiceAndOperationName": {"idx_service_op", queryByServiceAndOperationName}, 113 | "queryByServiceName": {"idx_service_name", queryByServiceName}, 114 | } 115 | ) 116 | 117 | type queryInfo struct { 118 | table string 119 | query string 120 | } 121 | 122 | func BuildQuery(queryName string, path schema.DbPath) string { 123 | if i, ok := m[queryName]; ok { 124 | return fmt.Sprintf(i.query, path.FullTable(i.table)) 125 | } 126 | panic("query not found") 127 | } 128 | 129 | func BuildPartitionQuery(queryName string, path schema.DbPath, part schema.PartitionKey) string { 130 | if i, ok := pm[queryName]; ok { 131 | ft := new(strings.Builder) 132 | ft.WriteString(path.Table(i.table)) 133 | ft.WriteString("_") 134 | ft.WriteString(part.Suffix()) 135 | return fmt.Sprintf(i.query, ft.String()) 136 | } 137 | panic("query not found") 138 | } 139 | -------------------------------------------------------------------------------- /storage/spanstore/reader/archive_reader_test.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "os" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/jaegertracing/jaeger/model" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/uber/jaeger-lib/metrics" 14 | 15 | "github.com/ydb-platform/jaeger-ydb-store/internal/testutil" 16 | "github.com/ydb-platform/jaeger-ydb-store/schema" 17 | ydbWriter "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/writer" 18 | ) 19 | 20 | func TestArchiveSpanReader_GetTrace(t *testing.T) { 21 | addArchiveTestData(t) 22 | s := setUpArchiveReader(t) 23 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 24 | defer cancel() 25 | trace, err := s.GetTrace(ctx, model.NewTraceID(1, 42)) 26 | if !assert.NoError(t, err) { 27 | return 28 | } 29 | 30 | assert.Len(t, trace.Spans, 2) 31 | assert.Equal(t, model.NewTraceID(1, 42), trace.Spans[0].TraceID) 32 | assert.Equal(t, model.SpanID(42), trace.Spans[0].SpanID) 33 | } 34 | 35 | var archiveOnce = new(sync.Once) 36 | 37 | func addArchiveTestData(t *testing.T) { 38 | archiveOnce.Do(func() { 39 | addArchiveTestDataOnce(t) 40 | }) 41 | } 42 | 43 | func addArchiveTestDataOnce(t *testing.T) { 44 | var err error 45 | opts := ydbWriter.SpanWriterOptions{ 46 | BatchWorkers: 1, 47 | BatchSize: 1, 48 | IndexerBufferSize: 100, 49 | IndexerMaxTraces: 10, 50 | IndexerTTL: time.Second, 51 | DbPath: schema.DbPath{Path: os.Getenv("YDB_PATH"), Folder: os.Getenv("YDB_FOLDER")}, 52 | WriteTimeout: time.Second, 53 | RetryAttemptTimeout: time.Second, 54 | ArchiveWriter: true, 55 | OpCacheSize: 256, 56 | } 57 | writer := ydbWriter.NewSpanWriter(testutil.YdbSessionPool(t), metrics.NullFactory, testutil.Zap(), testutil.JaegerLogger(), opts) 58 | 59 | spans := []*model.Span{ 60 | { 61 | TraceID: model.NewTraceID(1, 42), 62 | SpanID: model.NewSpanID(42), 63 | StartTime: time.Now(), 64 | Duration: time.Second, 65 | Process: &model.Process{ServiceName: "svc1"}, 66 | OperationName: "this-stuff", 67 | Tags: []model.KeyValue{ 68 | model.Int64("http.status_code", 200), 69 | model.String("some_tag", "some_value"), 70 | }, 71 | }, 72 | { 73 | TraceID: model.NewTraceID(rand.Uint64(), rand.Uint64()), 74 | SpanID: model.NewSpanID(1), 75 | StartTime: time.Now(), 76 | Duration: time.Second / 2, 77 | Process: &model.Process{ServiceName: "svc2"}, 78 | OperationName: "this-stuff", 79 | Tags: []model.KeyValue{ 80 | model.Int64("http.status_code", 200), 81 | model.String("some_tag", "some_value"), 82 | }, 83 | }, 84 | { 85 | TraceID: model.NewTraceID(1, 42), 86 | SpanID: model.NewSpanID(43), 87 | StartTime: time.Now(), 88 | Duration: time.Second, 89 | Process: &model.Process{ServiceName: "svc1"}, 90 | OperationName: "that-stuff", 91 | Tags: []model.KeyValue{ 92 | model.Int64("http.status_code", 404), 93 | }, 94 | }, 95 | { 96 | TraceID: model.NewTraceID(2, 42), 97 | SpanID: model.NewSpanID(1), 98 | StartTime: time.Now().Add(time.Hour * 2), 99 | Duration: time.Second*10 + time.Millisecond, 100 | Process: &model.Process{ServiceName: "svc2"}, 101 | OperationName: "that-stuff", 102 | Tags: []model.KeyValue{ 103 | model.Int64("http.status_code", 504), 104 | }, 105 | }, 106 | } 107 | for _, span := range spans { 108 | err = writer.WriteSpan(context.Background(), span) 109 | if !assert.NoError(t, err) { 110 | return 111 | } 112 | } 113 | // wait for flush 114 | <-time.After(time.Second * 2) 115 | } 116 | 117 | func setUpArchiveReader(t *testing.T) *SpanReader { 118 | return NewSpanReader( 119 | testutil.YdbSessionPool(t), 120 | SpanReaderOptions{ 121 | DbPath: schema.DbPath{Path: os.Getenv("YDB_PATH"), Folder: os.Getenv("YDB_FOLDER")}, 122 | ReadTimeout: time.Second * 10, 123 | QueryParallel: 10, 124 | ArchiveReader: true, 125 | }, 126 | testutil.Zap(), 127 | testutil.JaegerLogger(), 128 | ) 129 | } 130 | -------------------------------------------------------------------------------- /storage/spanstore/reader/cache.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type ttlCache struct { 9 | mx sync.RWMutex 10 | m map[interface{}]cacheValue 11 | } 12 | 13 | type cacheValue struct { 14 | value interface{} 15 | expires int64 16 | } 17 | 18 | func newTtlCache() *ttlCache { 19 | c := &ttlCache{ 20 | m: make(map[interface{}]cacheValue), 21 | } 22 | go c.evictProcess() 23 | return c 24 | } 25 | 26 | func (c *ttlCache) Get(key interface{}) (interface{}, bool) { 27 | c.mx.RLock() 28 | defer c.mx.RUnlock() 29 | if v, ok := c.m[key]; ok { 30 | return v.value, true 31 | } 32 | return nil, false 33 | } 34 | 35 | func (c *ttlCache) Set(key, value interface{}, ttl time.Duration) { 36 | c.mx.Lock() 37 | defer c.mx.Unlock() 38 | c.m[key] = cacheValue{ 39 | value: value, 40 | expires: time.Now().Add(ttl).Unix(), 41 | } 42 | } 43 | 44 | func (c *ttlCache) evictProcess() { 45 | for t := range time.Tick(time.Second) { 46 | c.mx.Lock() 47 | ts := t.Unix() 48 | for k, v := range c.m { 49 | if ts > v.expires { 50 | delete(c.m, k) 51 | } 52 | } 53 | c.mx.Unlock() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /storage/spanstore/reader/helpers.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "context" 5 | "sort" 6 | "sync" 7 | 8 | "github.com/ydb-platform/jaeger-ydb-store/schema" 9 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/dbmodel" 10 | ) 11 | 12 | type bucketOperation func(ctx context.Context, bucket uint8) 13 | 14 | func runBucketOperation(ctx context.Context, numBuckets uint8, callbackFunc bucketOperation) { 15 | wg := new(sync.WaitGroup) 16 | wg.Add(int(numBuckets)) 17 | for i := uint8(0); i < numBuckets; i++ { 18 | go func(ctx context.Context, bucket uint8) { 19 | defer wg.Done() 20 | callbackFunc(ctx, bucket) 21 | }(ctx, i) 22 | } 23 | wg.Wait() 24 | } 25 | 26 | type partitionOperation func(ctx context.Context, key schema.PartitionKey) 27 | 28 | func runPartitionOperation(ctx context.Context, parts []schema.PartitionKey, opFunc partitionOperation) { 29 | wg := new(sync.WaitGroup) 30 | wg.Add(len(parts)) 31 | for _, part := range parts { 32 | go func(ctx context.Context, part schema.PartitionKey) { 33 | defer wg.Done() 34 | opFunc(ctx, part) 35 | }(ctx, part) 36 | } 37 | wg.Wait() 38 | } 39 | 40 | type sharedResult struct { 41 | Rows []dbmodel.IndexResult 42 | Error error 43 | 44 | mx *sync.Mutex 45 | cancelCtx context.CancelFunc 46 | } 47 | 48 | func newSharedResult(cancelFunc context.CancelFunc) *sharedResult { 49 | return &sharedResult{ 50 | mx: new(sync.Mutex), 51 | cancelCtx: cancelFunc, 52 | Rows: make([]dbmodel.IndexResult, 0), 53 | } 54 | } 55 | 56 | func (r *sharedResult) AddRows(rows []dbmodel.IndexResult, err error) { 57 | r.mx.Lock() 58 | defer r.mx.Unlock() 59 | if err != nil { 60 | if r.Error == nil { 61 | r.Error = err 62 | } 63 | r.Rows = nil 64 | r.cancelCtx() 65 | return 66 | } 67 | for _, row := range rows { 68 | r.Rows = append(r.Rows, row) 69 | } 70 | } 71 | 72 | func (r *sharedResult) ProcessRows() (*dbmodel.UniqueTraceIDs, error) { 73 | if r.Error != nil { 74 | return nil, r.Error 75 | } 76 | sort.Slice(r.Rows, func(i, j int) bool { 77 | return r.Rows[i].RevTs < r.Rows[j].RevTs 78 | }) 79 | ids := dbmodel.NewUniqueTraceIDs() 80 | for _, row := range r.Rows { 81 | for _, id := range row.Ids { 82 | ids.Add(id) 83 | } 84 | } 85 | return ids, nil 86 | } 87 | -------------------------------------------------------------------------------- /storage/spanstore/reader/reader_test.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "os" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/jaegertracing/jaeger/model" 12 | "github.com/jaegertracing/jaeger/storage/spanstore" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/uber/jaeger-lib/metrics" 15 | 16 | "github.com/ydb-platform/jaeger-ydb-store/internal/testutil" 17 | "github.com/ydb-platform/jaeger-ydb-store/schema" 18 | ydbWriter "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/writer" 19 | ) 20 | 21 | func TestSpanReader_GetTrace(t *testing.T) { 22 | addTestData(t) 23 | s := setUpReader(t) 24 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 25 | defer cancel() 26 | trace, err := s.GetTrace(ctx, model.NewTraceID(1, 42)) 27 | if !assert.NoError(t, err) { 28 | return 29 | } 30 | 31 | assert.Len(t, trace.Spans, 2) 32 | assert.Equal(t, model.NewTraceID(1, 42), trace.Spans[0].TraceID) 33 | assert.Equal(t, model.SpanID(42), trace.Spans[0].SpanID) 34 | } 35 | 36 | func TestSpanReader_FindTraces(t *testing.T) { 37 | addTestData(t) 38 | s := setUpReader(t) 39 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 40 | defer cancel() 41 | _, err := s.FindTraces(ctx, &spanstore.TraceQueryParameters{ 42 | ServiceName: "svc1", 43 | StartTimeMin: time.Now(), 44 | StartTimeMax: time.Now(), 45 | }) 46 | assert.NoError(t, err) 47 | } 48 | 49 | func TestSpanReader_FindTraceIDs(t *testing.T) { 50 | addTestData(t) 51 | s := setUpReader(t) 52 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 53 | defer cancel() 54 | 55 | t.Run("duration", func(t *testing.T) { 56 | traces, err := s.FindTraces(ctx, &spanstore.TraceQueryParameters{ 57 | ServiceName: "svc2", 58 | StartTimeMin: time.Now().Add(-time.Hour), 59 | StartTimeMax: time.Now().Add(time.Hour * 3), 60 | DurationMin: time.Second * 9, 61 | DurationMax: time.Second * 12, 62 | }) 63 | if !assert.NoError(t, err) { 64 | return 65 | } 66 | if !assert.Len(t, traces, 1) { 67 | return 68 | } 69 | assert.Equal(t, "http.status_code", traces[0].Spans[0].Tags[0].Key) 70 | assert.Equal(t, "504", traces[0].Spans[0].Tags[0].AsString()) 71 | }) 72 | t.Run("service_name", func(t *testing.T) { 73 | ids, err := s.FindTraceIDs(ctx, &spanstore.TraceQueryParameters{ 74 | ServiceName: "svc1", 75 | StartTimeMin: time.Now().Add(-time.Hour), 76 | StartTimeMax: time.Now().Add(time.Hour * 2), 77 | }) 78 | if !assert.NoError(t, err) { 79 | return 80 | } 81 | assert.Len(t, ids, 1) 82 | }) 83 | t.Run("service_and_operation", func(t *testing.T) { 84 | ids, err := s.FindTraceIDs(ctx, &spanstore.TraceQueryParameters{ 85 | ServiceName: "svc1", 86 | OperationName: "this-stuff", 87 | StartTimeMin: time.Now().Add(-time.Hour), 88 | StartTimeMax: time.Now().Add(time.Hour), 89 | }) 90 | assert.Len(t, ids, 1) 91 | assert.NoError(t, err) 92 | }) 93 | t.Run("tags", func(t *testing.T) { 94 | ids, err := s.FindTraceIDs(ctx, &spanstore.TraceQueryParameters{ 95 | ServiceName: "svc1", 96 | StartTimeMin: time.Now().Add(-time.Hour), 97 | StartTimeMax: time.Now().Add(time.Hour), 98 | Tags: map[string]string{ 99 | "some_tag": "some_value", 100 | }, 101 | }) 102 | assert.Len(t, ids, 1) 103 | assert.NoError(t, err) 104 | }) 105 | } 106 | 107 | func TestSpanReader_GetServices(t *testing.T) { 108 | addTestData(t) 109 | s := setUpReader(t) 110 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 111 | defer cancel() 112 | services, err := s.GetServices(ctx) 113 | assert.NoError(t, err) 114 | if err != nil { 115 | return 116 | } 117 | assert.Contains(t, services, "svc1") 118 | assert.Contains(t, services, "svc2") 119 | } 120 | 121 | func TestSpanReader_GetOperations(t *testing.T) { 122 | addTestData(t) 123 | s := setUpReader(t) 124 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 125 | defer cancel() 126 | ops, err := s.GetOperations(ctx, spanstore.OperationQueryParameters{ServiceName: "svc1"}) 127 | assert.NoError(t, err) 128 | if err != nil { 129 | return 130 | } 131 | assert.Len(t, ops, 2) 132 | 133 | opNames := make(map[string]bool) 134 | for _, op := range ops { 135 | opNames[op.Name] = true 136 | } 137 | assert.Len(t, opNames, 2) 138 | assert.Contains(t, opNames, "this-stuff") 139 | assert.Contains(t, opNames, "that-stuff") 140 | } 141 | 142 | var once = new(sync.Once) 143 | 144 | func addTestData(t *testing.T) { 145 | once.Do(func() { 146 | addTestDataOnce(t) 147 | }) 148 | } 149 | 150 | func addTestDataOnce(t *testing.T) { 151 | var err error 152 | opts := ydbWriter.SpanWriterOptions{ 153 | BatchWorkers: 1, 154 | BatchSize: 1, 155 | IndexerBufferSize: 100, 156 | IndexerMaxTraces: 10, 157 | IndexerTTL: time.Second, 158 | DbPath: schema.DbPath{Path: os.Getenv("YDB_PATH"), Folder: os.Getenv("YDB_FOLDER")}, 159 | WriteTimeout: time.Second, 160 | RetryAttemptTimeout: time.Second, 161 | OpCacheSize: 256, 162 | } 163 | writer := ydbWriter.NewSpanWriter(testutil.YdbSessionPool(t), metrics.NullFactory, testutil.Zap(), testutil.JaegerLogger(), opts) 164 | 165 | spans := []*model.Span{ 166 | { 167 | TraceID: model.NewTraceID(1, 42), 168 | SpanID: model.NewSpanID(42), 169 | StartTime: time.Now(), 170 | Duration: time.Second, 171 | Process: &model.Process{ServiceName: "svc1"}, 172 | OperationName: "this-stuff", 173 | Tags: []model.KeyValue{ 174 | model.Int64("http.status_code", 200), 175 | model.String("some_tag", "some_value"), 176 | }, 177 | }, 178 | { 179 | TraceID: model.NewTraceID(rand.Uint64(), rand.Uint64()), 180 | SpanID: model.NewSpanID(1), 181 | StartTime: time.Now(), 182 | Duration: time.Second / 2, 183 | Process: &model.Process{ServiceName: "svc2"}, 184 | OperationName: "this-stuff", 185 | Tags: []model.KeyValue{ 186 | model.Int64("http.status_code", 200), 187 | model.String("some_tag", "some_value"), 188 | }, 189 | }, 190 | { 191 | TraceID: model.NewTraceID(1, 42), 192 | SpanID: model.NewSpanID(43), 193 | StartTime: time.Now(), 194 | Duration: time.Second, 195 | Process: &model.Process{ServiceName: "svc1"}, 196 | OperationName: "that-stuff", 197 | Tags: []model.KeyValue{ 198 | model.Int64("http.status_code", 404), 199 | }, 200 | }, 201 | { 202 | TraceID: model.NewTraceID(2, 42), 203 | SpanID: model.NewSpanID(1), 204 | StartTime: time.Now().Add(time.Hour * 2), 205 | Duration: time.Second*10 + time.Millisecond, 206 | Process: &model.Process{ServiceName: "svc2"}, 207 | OperationName: "that-stuff", 208 | Tags: []model.KeyValue{ 209 | model.Int64("http.status_code", 504), 210 | }, 211 | }, 212 | } 213 | for _, span := range spans { 214 | err = writer.WriteSpan(context.Background(), span) 215 | if !assert.NoError(t, err) { 216 | return 217 | } 218 | } 219 | // wait for flush 220 | <-time.After(time.Second * 2) 221 | } 222 | 223 | func setUpReader(t *testing.T) *SpanReader { 224 | return NewSpanReader( 225 | testutil.YdbSessionPool(t), 226 | SpanReaderOptions{ 227 | DbPath: schema.DbPath{Path: os.Getenv("YDB_PATH"), Folder: os.Getenv("YDB_FOLDER")}, 228 | ReadTimeout: time.Second * 10, 229 | QueryParallel: 10, 230 | OpLimit: 100, 231 | SvcLimit: 100, 232 | }, 233 | testutil.Zap(), 234 | testutil.JaegerLogger(), 235 | ) 236 | } 237 | -------------------------------------------------------------------------------- /storage/spanstore/writer/archive_writer.go: -------------------------------------------------------------------------------- 1 | package writer 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/hashicorp/go-hclog" 8 | "github.com/jaegertracing/jaeger/model" 9 | "github.com/uber/jaeger-lib/metrics" 10 | "github.com/ydb-platform/ydb-go-sdk/v3/table" 11 | "github.com/ydb-platform/ydb-go-sdk/v3/table/types" 12 | "go.uber.org/zap" 13 | 14 | "github.com/ydb-platform/jaeger-ydb-store/internal/db" 15 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/dbmodel" 16 | wmetrics "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/writer/metrics" 17 | ) 18 | 19 | const ( 20 | tblArchive = "archive" 21 | ) 22 | 23 | type ArchiveSpanWriter struct { 24 | metrics batchWriterMetrics 25 | pool table.Client 26 | logger *zap.Logger 27 | jaegerLogger hclog.Logger 28 | opts BatchWriterOptions 29 | } 30 | 31 | func NewArchiveWriter(pool table.Client, factory metrics.Factory, logger *zap.Logger, jaegerLogger hclog.Logger, opts BatchWriterOptions) *ArchiveSpanWriter { 32 | ns := factory.Namespace(metrics.NSOptions{Name: "archive"}) 33 | 34 | return &ArchiveSpanWriter{ 35 | pool: pool, 36 | logger: logger, 37 | jaegerLogger: jaegerLogger, 38 | opts: opts, 39 | metrics: newBatchWriterMetrics(ns), 40 | } 41 | } 42 | 43 | func (w *ArchiveSpanWriter) WriteItems(items []interface{}) { 44 | spans := make([]*model.Span, 0, len(items)) 45 | for _, item := range items { 46 | span := item.(*model.Span) 47 | spans = append(spans, span) 48 | } 49 | w.writeItems(spans) 50 | } 51 | 52 | func (w *ArchiveSpanWriter) writeItems(items []*model.Span) { 53 | spanRecords := make([]types.Value, 0, len(items)) 54 | for _, span := range items { 55 | dbSpan, _ := dbmodel.FromDomain(span) 56 | spanRecords = append(spanRecords, dbSpan.StructValue()) 57 | } 58 | 59 | tableName := w.opts.DbPath.FullTable(tblArchive) 60 | var err error 61 | 62 | if err = w.uploadRows(tableName, spanRecords, w.metrics.traces); err != nil { 63 | w.logger.Error("insertSpan error", zap.Error(err)) 64 | w.jaegerLogger.Error( 65 | "Failed to save spans to archive storage", 66 | "error", err, 67 | ) 68 | 69 | return 70 | } 71 | } 72 | 73 | func (w *ArchiveSpanWriter) uploadRows(tableName string, rows []types.Value, metrics *wmetrics.WriteMetrics) error { 74 | ts := time.Now() 75 | data := types.ListValue(rows...) 76 | 77 | ctx := context.Background() 78 | if w.opts.WriteTimeout > 0 { 79 | var cancel context.CancelFunc 80 | ctx, cancel = context.WithTimeout(ctx, w.opts.WriteTimeout) 81 | defer cancel() 82 | } 83 | err := db.UpsertData(ctx, w.pool, tableName, data, w.opts.RetryAttemptTimeout) 84 | metrics.Emit(err, time.Since(ts), len(rows)) 85 | return err 86 | } 87 | -------------------------------------------------------------------------------- /storage/spanstore/writer/archive_writer_test.go: -------------------------------------------------------------------------------- 1 | package writer 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/jaegertracing/jaeger/model" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | "github.com/uber/jaeger-lib/metrics" 13 | 14 | "github.com/ydb-platform/jaeger-ydb-store/internal/testutil" 15 | "github.com/ydb-platform/jaeger-ydb-store/schema" 16 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/reader" 17 | ) 18 | 19 | func TestArchiveSpanWriter_WriteSpan(t *testing.T) { 20 | var err error 21 | pool := testutil.YdbSessionPool(t) 22 | opts := SpanWriterOptions{ 23 | BufferSize: 10, 24 | BatchWorkers: 1, 25 | BatchSize: 1, 26 | IndexerBufferSize: 10, 27 | IndexerMaxTraces: 1, 28 | IndexerTTL: time.Second, 29 | DbPath: schema.DbPath{Path: os.Getenv("YDB_PATH"), Folder: os.Getenv("YDB_FOLDER")}, 30 | WriteTimeout: time.Second, 31 | RetryAttemptTimeout: time.Second, 32 | ArchiveWriter: true, 33 | OpCacheSize: 256, 34 | } 35 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 36 | defer cancel() 37 | 38 | dt := time.Date(2063, 4, 5, 0, 0, 0, 0, time.UTC) 39 | err = testutil.CreatePartitionTables(ctx, pool, schema.PartitionFromTime(dt)) 40 | require.NoError(t, err) 41 | 42 | testTraceId := model.NewTraceID(1, 47) 43 | writer := NewSpanWriter(pool, metrics.NullFactory, testutil.Zap(), testutil.JaegerLogger(), opts) 44 | span := &model.Span{ 45 | TraceID: testTraceId, 46 | SpanID: model.NewSpanID(1), 47 | StartTime: dt, 48 | OperationName: "salute a Vulcan", 49 | Process: model.NewProcess("svc", nil), 50 | Tags: []model.KeyValue{ 51 | model.Int64("foo", 42), 52 | model.String("bar", "baz"), 53 | }, 54 | } 55 | err = writer.WriteSpan(context.Background(), span) 56 | if !assert.NoError(t, err) { 57 | return 58 | } 59 | <-time.After(time.Second * 5) 60 | 61 | ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) 62 | defer cancel() 63 | r := setUpArchiveReader(t) 64 | trace, err := r.GetTrace(ctx, testTraceId) 65 | if !assert.NoError(t, err) { 66 | return 67 | } 68 | assert.NotEmpty(t, trace) 69 | assert.Equal(t, "svc", span.Process.ServiceName) 70 | span = trace.FindSpanByID(model.NewSpanID(1)) 71 | assert.NotEmpty(t, span) 72 | } 73 | 74 | func setUpArchiveReader(t *testing.T) *reader.SpanReader { 75 | return reader.NewSpanReader( 76 | testutil.YdbSessionPool(t), 77 | reader.SpanReaderOptions{ 78 | DbPath: schema.DbPath{Path: os.Getenv("YDB_PATH"), Folder: os.Getenv("YDB_FOLDER")}, 79 | ReadTimeout: time.Second * 10, 80 | QueryParallel: 10, 81 | ArchiveReader: true, 82 | }, 83 | testutil.Zap(), 84 | testutil.JaegerLogger(), 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /storage/spanstore/writer/batch_writer.go: -------------------------------------------------------------------------------- 1 | package writer 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/hashicorp/go-hclog" 8 | "github.com/jaegertracing/jaeger/model" 9 | "github.com/uber/jaeger-lib/metrics" 10 | "github.com/ydb-platform/ydb-go-sdk/v3/table" 11 | "github.com/ydb-platform/ydb-go-sdk/v3/table/types" 12 | "go.uber.org/zap" 13 | 14 | "github.com/ydb-platform/jaeger-ydb-store/internal/db" 15 | "github.com/ydb-platform/jaeger-ydb-store/schema" 16 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/dbmodel" 17 | wmetrics "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/writer/metrics" 18 | ) 19 | 20 | const ( 21 | tblTraces = "traces" 22 | ) 23 | 24 | type BatchSpanWriter struct { 25 | metrics batchWriterMetrics 26 | pool table.Client 27 | logger *zap.Logger 28 | jaegerLogger hclog.Logger 29 | opts BatchWriterOptions 30 | } 31 | 32 | func NewBatchWriter(pool table.Client, factory metrics.Factory, logger *zap.Logger, jaegerLogger hclog.Logger, opts BatchWriterOptions) *BatchSpanWriter { 33 | return &BatchSpanWriter{ 34 | pool: pool, 35 | logger: logger, 36 | jaegerLogger: jaegerLogger, 37 | opts: opts, 38 | metrics: newBatchWriterMetrics(factory), 39 | } 40 | } 41 | 42 | func (w *BatchSpanWriter) WriteItems(items []interface{}) { 43 | parts := map[schema.PartitionKey][]*model.Span{} 44 | for _, item := range items { 45 | span := item.(*model.Span) 46 | k := schema.PartitionFromTime(span.StartTime) 47 | parts[k] = append(parts[k], span) 48 | } 49 | for k, partial := range parts { 50 | w.writeItemsToPartition(k, partial) 51 | } 52 | } 53 | 54 | func (w *BatchSpanWriter) writeItemsToPartition(part schema.PartitionKey, items []*model.Span) { 55 | spanRecords := make([]types.Value, 0, len(items)) 56 | for _, span := range items { 57 | dbSpan, _ := dbmodel.FromDomain(span) 58 | spanRecords = append(spanRecords, dbSpan.StructValue()) 59 | } 60 | 61 | tableName := func(table string) string { 62 | return part.BuildFullTableName(w.opts.DbPath.String(), table) 63 | } 64 | var err error 65 | 66 | if err = w.uploadRows(tableName(tblTraces), spanRecords, w.metrics.traces); err != nil { 67 | w.logger.Error("insertSpan error", zap.Error(err)) 68 | w.jaegerLogger.Error( 69 | "Failed to save spans", 70 | "error", err, 71 | ) 72 | return 73 | } 74 | } 75 | 76 | func (w *BatchSpanWriter) uploadRows(tableName string, rows []types.Value, metrics *wmetrics.WriteMetrics) error { 77 | ts := time.Now() 78 | 79 | data := types.ListValue(rows...) 80 | 81 | ctx := context.Background() 82 | if w.opts.WriteTimeout > 0 { 83 | var cancel context.CancelFunc 84 | ctx, cancel = context.WithTimeout(ctx, w.opts.WriteTimeout) 85 | defer cancel() 86 | } 87 | err := db.UpsertData(ctx, w.pool, tableName, data, w.opts.RetryAttemptTimeout) 88 | metrics.Emit(err, time.Since(ts), len(rows)) 89 | return err 90 | } 91 | -------------------------------------------------------------------------------- /storage/spanstore/writer/metrics.go: -------------------------------------------------------------------------------- 1 | package writer 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/uber/jaeger-lib/metrics" 7 | 8 | wmetrics "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/writer/metrics" 9 | ) 10 | 11 | type batchWriterMetrics struct { 12 | traces *wmetrics.WriteMetrics 13 | spansDropped metrics.Counter 14 | } 15 | 16 | func newBatchWriterMetrics(factory metrics.Factory) batchWriterMetrics { 17 | return batchWriterMetrics{ 18 | traces: wmetrics.NewWriteMetrics(factory, "traces"), 19 | spansDropped: factory.Counter(metrics.Options{Name: "spans_dropped"}), 20 | } 21 | } 22 | 23 | type spanMetricsKey struct { 24 | svc string 25 | op string 26 | } 27 | 28 | type invalidSpanMetrics struct { 29 | mf metrics.Factory 30 | m map[spanMetricsKey]metrics.Counter 31 | mu sync.Mutex 32 | } 33 | 34 | func newInvalidSpanMetrics(mf metrics.Factory) *invalidSpanMetrics { 35 | return &invalidSpanMetrics{ 36 | mf: mf, 37 | m: make(map[spanMetricsKey]metrics.Counter, 0), 38 | } 39 | } 40 | 41 | func (m *invalidSpanMetrics) Inc(svc, op string) { 42 | m.mu.Lock() 43 | defer m.mu.Unlock() 44 | k := spanMetricsKey{svc: svc, op: op} 45 | if _, exists := m.m[k]; !exists { 46 | m.m[k] = m.mf.Counter(metrics.Options{Name: "invalid_spans", Tags: map[string]string{"svc": svc, "op": op}}) 47 | } 48 | m.m[k].Inc(1) 49 | } 50 | -------------------------------------------------------------------------------- /storage/spanstore/writer/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/uber/jaeger-lib/metrics" 7 | ) 8 | 9 | type WriteMetrics struct { 10 | Attempts metrics.Counter `metric:"attempts"` 11 | Inserts metrics.Counter `metric:"inserts"` 12 | Errors metrics.Counter `metric:"errors"` 13 | LatencyOk metrics.Timer `metric:"latency-ok"` 14 | LatencyErr metrics.Timer `metric:"latency-err"` 15 | RecordsOk metrics.Counter `metric:"records-ok"` 16 | RecordsErr metrics.Counter `metric:"records-err"` 17 | } 18 | 19 | func NewWriteMetrics(factory metrics.Factory, tableName string) *WriteMetrics { 20 | t := &WriteMetrics{} 21 | metrics.Init(t, factory.Namespace(metrics.NSOptions{Name: tableName, Tags: nil}), nil) 22 | return t 23 | } 24 | 25 | func (t *WriteMetrics) Emit(err error, latency time.Duration, count int) { 26 | t.Attempts.Inc(1) 27 | if err != nil { 28 | t.LatencyErr.Record(latency) 29 | t.Errors.Inc(1) 30 | t.RecordsErr.Inc(int64(count)) 31 | } else { 32 | t.LatencyOk.Record(latency) 33 | t.Inserts.Inc(1) 34 | t.RecordsOk.Inc(int64(count)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /storage/spanstore/writer/options.go: -------------------------------------------------------------------------------- 1 | package writer 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ydb-platform/jaeger-ydb-store/schema" 7 | ) 8 | 9 | type BatchWriterOptions struct { 10 | DbPath schema.DbPath 11 | WriteTimeout time.Duration 12 | RetryAttemptTimeout time.Duration 13 | } 14 | 15 | type SpanWriterOptions struct { 16 | BufferSize int 17 | BatchSize int 18 | BatchWorkers int 19 | IndexerBufferSize int 20 | IndexerMaxTraces int 21 | IndexerTTL time.Duration 22 | DbPath schema.DbPath 23 | WriteTimeout time.Duration 24 | RetryAttemptTimeout time.Duration 25 | ArchiveWriter bool 26 | OpCacheSize int 27 | MaxSpanAge time.Duration 28 | } 29 | -------------------------------------------------------------------------------- /storage/spanstore/writer/writer.go: -------------------------------------------------------------------------------- 1 | package writer 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/hashicorp/go-hclog" 8 | lru "github.com/hashicorp/golang-lru" 9 | "github.com/jaegertracing/jaeger/model" 10 | "github.com/uber/jaeger-lib/metrics" 11 | "github.com/ydb-platform/ydb-go-sdk/v3/table" 12 | "github.com/ydb-platform/ydb-go-sdk/v3/table/types" 13 | "go.uber.org/zap" 14 | 15 | "github.com/ydb-platform/jaeger-ydb-store/internal/db" 16 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/batch" 17 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/indexer" 18 | ) 19 | 20 | // SpanWriter handles all span/indexer writes to YDB 21 | type SpanWriter struct { 22 | opts SpanWriterOptions 23 | pool table.Client 24 | logger *zap.Logger 25 | jaegerLogger hclog.Logger 26 | spanBatch *batch.Queue 27 | indexer *indexer.Indexer 28 | nameCache *lru.Cache 29 | invalidateMetrics *invalidSpanMetrics 30 | } 31 | 32 | // NewSpanWriter creates writer interface implementation for YDB 33 | func NewSpanWriter(pool table.Client, metricsFactory metrics.Factory, logger *zap.Logger, jaegerLogger hclog.Logger, opts SpanWriterOptions) *SpanWriter { 34 | cache, _ := lru.New(opts.OpCacheSize) // it's ok to ignore this error for negative size 35 | batchOpts := batch.Options{ 36 | BufferSize: opts.BufferSize, 37 | BatchSize: opts.BatchSize, 38 | BatchWorkers: opts.BatchWorkers, 39 | } 40 | writerOpts := BatchWriterOptions{ 41 | WriteTimeout: opts.WriteTimeout, 42 | RetryAttemptTimeout: opts.RetryAttemptTimeout, 43 | DbPath: opts.DbPath, 44 | } 45 | var batchWriter batch.Writer 46 | if opts.ArchiveWriter { 47 | batchWriter = NewArchiveWriter(pool, metricsFactory, logger, jaegerLogger, writerOpts) 48 | } else { 49 | batchWriter = NewBatchWriter(pool, metricsFactory, logger, jaegerLogger, writerOpts) 50 | } 51 | bq := batch.NewQueue(batchOpts, metricsFactory.Namespace(metrics.NSOptions{Name: "spans"}), batchWriter) 52 | idx := indexer.NewIndexer(pool, metricsFactory, logger, jaegerLogger, indexer.Options{ 53 | DbPath: opts.DbPath, 54 | BufferSize: opts.IndexerBufferSize, 55 | MaxTraces: opts.IndexerMaxTraces, 56 | MaxTTL: opts.IndexerTTL, 57 | WriteTimeout: opts.WriteTimeout, 58 | RetryAttemptTimeout: opts.RetryAttemptTimeout, 59 | Batch: batchOpts, 60 | }) 61 | return &SpanWriter{ 62 | opts: opts, 63 | pool: pool, 64 | logger: logger, 65 | jaegerLogger: jaegerLogger, 66 | spanBatch: bq, 67 | indexer: idx, 68 | nameCache: cache, 69 | invalidateMetrics: newInvalidSpanMetrics(metricsFactory), 70 | } 71 | } 72 | 73 | // WriteSpan saves the span into YDB 74 | func (s *SpanWriter) WriteSpan(ctx context.Context, span *model.Span) error { 75 | if s.opts.MaxSpanAge != time.Duration(0) && time.Now().Sub(span.StartTime) > s.opts.MaxSpanAge { 76 | s.invalidateMetrics.Inc(span.Process.ServiceName, span.OperationName) 77 | return nil 78 | } 79 | if span.StartTime.Unix() == 0 || span.StartTime.IsZero() { 80 | s.invalidateMetrics.Inc(span.Process.ServiceName, span.OperationName) 81 | return nil 82 | } 83 | err := s.spanBatch.Add(span) 84 | if err != nil { 85 | switch err { 86 | case batch.ErrOverflow: 87 | return nil 88 | default: 89 | return err 90 | } 91 | } 92 | 93 | if !s.opts.ArchiveWriter { 94 | _ = s.indexer.Add(span) 95 | } 96 | 97 | return s.saveServiceNameAndOperationName(ctx, span) 98 | } 99 | 100 | func (s *SpanWriter) saveServiceNameAndOperationName(ctx context.Context, span *model.Span) error { 101 | serviceName := span.GetProcess().GetServiceName() 102 | operationName := span.GetOperationName() 103 | kind, _ := span.GetSpanKind() 104 | if exists, _ := s.nameCache.ContainsOrAdd(serviceName, true); !exists { 105 | data := types.ListValue(types.StructValue( 106 | types.StructFieldValue("service_name", types.TextValue(serviceName)), 107 | )) 108 | 109 | if s.opts.WriteTimeout > 0 { 110 | var cancel context.CancelFunc 111 | ctx, cancel = context.WithTimeout(ctx, s.opts.WriteTimeout) 112 | defer cancel() 113 | } 114 | err := db.UpsertData(ctx, s.pool, s.opts.DbPath.FullTable("service_names"), data, s.opts.RetryAttemptTimeout) 115 | if err != nil { 116 | s.jaegerLogger.Error( 117 | "Failed to save service name", 118 | "service_name", serviceName, 119 | "error", err, 120 | ) 121 | 122 | return err 123 | } 124 | } 125 | if operationName == "" { 126 | return nil 127 | } 128 | if exists, _ := s.nameCache.ContainsOrAdd(serviceName+"-"+operationName+"-"+kind.String(), true); !exists { 129 | data := types.ListValue(types.StructValue( 130 | types.StructFieldValue("service_name", types.TextValue(serviceName)), 131 | types.StructFieldValue("operation_name", types.TextValue(operationName)), 132 | types.StructFieldValue("span_kind", types.TextValue(kind.String())), 133 | )) 134 | if s.opts.WriteTimeout > 0 { 135 | var cancel context.CancelFunc 136 | ctx, cancel = context.WithTimeout(ctx, s.opts.WriteTimeout) 137 | defer cancel() 138 | } 139 | err := db.UpsertData(ctx, s.pool, s.opts.DbPath.FullTable("operation_names_v2"), data, s.opts.RetryAttemptTimeout) 140 | if err != nil { 141 | s.jaegerLogger.Error( 142 | "Failed to save operation name", 143 | "operation_name", operationName, 144 | "error", err, 145 | ) 146 | return err 147 | } 148 | } 149 | return nil 150 | } 151 | 152 | func (s *SpanWriter) Close() { 153 | s.spanBatch.Close() 154 | s.indexer.Close() 155 | } 156 | -------------------------------------------------------------------------------- /storage/spanstore/writer/writer_test.go: -------------------------------------------------------------------------------- 1 | package writer 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/jaegertracing/jaeger/model" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | "github.com/uber/jaeger-lib/metrics" 13 | 14 | "github.com/ydb-platform/jaeger-ydb-store/internal/testutil" 15 | "github.com/ydb-platform/jaeger-ydb-store/schema" 16 | "github.com/ydb-platform/jaeger-ydb-store/storage/spanstore/reader" 17 | ) 18 | 19 | func TestSpanWriter_WriteSpan(t *testing.T) { 20 | var err error 21 | pool := testutil.YdbSessionPool(t) 22 | opts := SpanWriterOptions{ 23 | BufferSize: 10, 24 | BatchWorkers: 1, 25 | BatchSize: 1, 26 | IndexerBufferSize: 10, 27 | IndexerMaxTraces: 1, 28 | IndexerTTL: time.Second, 29 | DbPath: schema.DbPath{Path: os.Getenv("YDB_PATH"), Folder: os.Getenv("YDB_FOLDER")}, 30 | WriteTimeout: time.Second, 31 | RetryAttemptTimeout: time.Second, 32 | OpCacheSize: 256, 33 | } 34 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 35 | defer cancel() 36 | 37 | dt := time.Date(2063, 4, 5, 0, 0, 0, 0, time.UTC) 38 | err = testutil.CreatePartitionTables(ctx, pool, schema.PartitionFromTime(dt)) 39 | require.NoError(t, err) 40 | 41 | testTraceId := model.NewTraceID(1, 47) 42 | writer := NewSpanWriter(pool, metrics.NullFactory, testutil.Zap(), testutil.JaegerLogger(), opts) 43 | span := &model.Span{ 44 | TraceID: testTraceId, 45 | SpanID: model.NewSpanID(1), 46 | StartTime: dt, 47 | OperationName: "salute a Vulcan", 48 | Process: model.NewProcess("svc", nil), 49 | Tags: []model.KeyValue{ 50 | model.Int64("foo", 42), 51 | model.String("bar", "baz"), 52 | }, 53 | } 54 | err = writer.WriteSpan(context.Background(), span) 55 | if !assert.NoError(t, err) { 56 | return 57 | } 58 | <-time.After(time.Second * 5) 59 | 60 | ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) 61 | defer cancel() 62 | r := setUpReader(t) 63 | trace, err := r.GetTrace(ctx, testTraceId) 64 | if !assert.NoError(t, err) { 65 | return 66 | } 67 | assert.NotEmpty(t, trace) 68 | assert.Equal(t, "svc", span.Process.ServiceName) 69 | span = trace.FindSpanByID(model.NewSpanID(1)) 70 | assert.NotEmpty(t, span) 71 | } 72 | 73 | func setUpReader(t *testing.T) *reader.SpanReader { 74 | return reader.NewSpanReader( 75 | testutil.YdbSessionPool(t), 76 | reader.SpanReaderOptions{ 77 | DbPath: schema.DbPath{Path: os.Getenv("YDB_PATH"), Folder: os.Getenv("YDB_FOLDER")}, 78 | ReadTimeout: time.Second * 10, 79 | QueryParallel: 10, 80 | }, 81 | testutil.Zap(), 82 | testutil.JaegerLogger(), 83 | ) 84 | } 85 | --------------------------------------------------------------------------------