├── .all-contributorsrc ├── .devcontainer └── devcontainer.json ├── .github ├── dependabot.yml └── workflows │ ├── golang.yaml │ ├── release-please.yaml │ └── release.yaml ├── .gitignore ├── .ko.yaml ├── .release-please-manifest.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── charts └── jaeger-postgresql │ ├── .gitignore │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── _helpers.tpl │ ├── cleaner.yaml │ ├── deployment.yaml │ └── service.yaml │ └── values-template.yaml ├── cmd ├── jaeger-postgresql-cleaner │ └── main.go └── jaeger-postgresql │ └── main.go ├── docker-compose.yaml ├── go.mod ├── go.sum ├── hack ├── .gitignore ├── config.yaml └── run.sh ├── internal ├── logger │ └── logger.go ├── sql │ ├── copyfrom.go │ ├── db.go │ ├── migrations.go │ ├── migrations │ │ ├── 001_initial.sql │ │ └── 002_indexes.sql │ ├── models.go │ ├── query.sql │ ├── query.sql.go │ └── query_test.go ├── sqltest │ └── helpers.go └── store │ ├── instrumentation.go │ ├── integration_test.go │ ├── mapping.go │ ├── mapping_test.go │ ├── reader.go │ └── writer.go ├── release-please-config.json └── sqlc.yaml /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "commitType": "docs", 8 | "commitConvention": "angular", 9 | "contributors": [ 10 | { 11 | "login": "jozef-slezak", 12 | "name": "Jozef Slezak", 13 | "avatar_url": "https://avatars.githubusercontent.com/u/16844103?v=4", 14 | "profile": "https://github.com/jozef-slezak", 15 | "contributions": [ 16 | "code" 17 | ] 18 | }, 19 | { 20 | "login": "robbert229", 21 | "name": "John Rowley", 22 | "avatar_url": "https://avatars.githubusercontent.com/u/3454480?v=4", 23 | "profile": "http://blog.johnrowley.co", 24 | "contributions": [ 25 | "code" 26 | ] 27 | }, 28 | { 29 | "login": "aokiji", 30 | "name": "Nicolas De los Santos", 31 | "avatar_url": "https://avatars.githubusercontent.com/u/164521?v=4", 32 | "profile": "https://github.com/aokiji", 33 | "contributions": [ 34 | "code" 35 | ] 36 | } 37 | ], 38 | "contributorsPerLine": 7, 39 | "skipCi": true, 40 | "repoType": "github", 41 | "repoHost": "https://github.com", 42 | "projectName": "jaeger-postgresql", 43 | "projectOwner": "robbert229" 44 | } 45 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/go 3 | { 4 | "name": "Go", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/go:1.21", 7 | // Features to add to the dev container. More info: https://containers.dev/features. 8 | "features": { 9 | "ghcr.io/robbert229/devcontainer-features/operator-sdk:latest": {}, 10 | "ghcr.io/robbert229/devcontainer-features/postgresql-client:1": {}, 11 | "ghcr.io/rio/features/k3d:latest": {}, 12 | "ghcr.io/rio/features/k9s:1.1.5": {}, 13 | "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, 14 | "ghcr.io/devcontainers-contrib/features/kubectl-asdf:2":{} 15 | }, 16 | // Configure tool-specific properties. 17 | "customizations": { 18 | // Configure properties specific to VS Code. 19 | "vscode": { 20 | "settings": {}, 21 | "extensions": [ 22 | "streetsidesoftware.code-spell-checker", 23 | "ms-kubernetes-tools.vscode-kubernetes-tools" 24 | ] 25 | } 26 | }, 27 | "runArgs": [ 28 | "--network=host" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/golang.yaml: -------------------------------------------------------------------------------- 1 | name: golang 2 | on: [push] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | # Service containers to run with `container-job` 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Setup Go 11 | uses: actions/setup-go@v4 12 | with: 13 | go-version: '1.21.x' 14 | - name: Install dependencies 15 | run: go mod download 16 | - name: Lint 17 | run: go vet ./... 18 | - name: Build 19 | run: go build -v ./... 20 | - name: Test 21 | run: go test ./... 22 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | permissions: 7 | contents: write 8 | pull-requests: write 9 | 10 | name: release-please 11 | 12 | jobs: 13 | release-please: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: google-github-actions/release-please-action@v4 17 | id: release-please 18 | with: 19 | release-type: simple -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: releaser 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - v* 8 | release: 9 | types: [published] 10 | 11 | permissions: 12 | contents: write 13 | packages: write 14 | issues: write 15 | id-token: write 16 | 17 | jobs: 18 | publish: 19 | runs-on: ubuntu-latest 20 | env: 21 | flags: "" 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - name: Set up Go 28 | uses: actions/setup-go@v4 29 | - uses: ko-build/setup-ko@v0.6 30 | - name: publish 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | run: | 34 | make publish VERSION=${{ github.ref_name }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | 17 | jaeger/ 18 | jaeger-all-in-one 19 | 20 | *.tgz -------------------------------------------------------------------------------- /.ko.yaml: -------------------------------------------------------------------------------- 1 | defaultPlatforms: 2 | - linux/arm64 3 | - linux/amd64 4 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "1.8.0" 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.8.0](https://github.com/robbert229/jaeger-postgresql/compare/v1.7.0...v1.8.0) (2025-01-29) 4 | 5 | 6 | ### Features 7 | 8 | * add arm64 support ([#75](https://github.com/robbert229/jaeger-postgresql/issues/75)) ([e45676e](https://github.com/robbert229/jaeger-postgresql/commit/e45676e3a2d2d61161f312427ababf562b9d96e6)) 9 | 10 | 11 | ### Performance Improvements 12 | 13 | * optimize span count metric gathering ([#65](https://github.com/robbert229/jaeger-postgresql/issues/65)) ([56f381c](https://github.com/robbert229/jaeger-postgresql/commit/56f381c53c047fc954293929c63eba7d9e379283)) 14 | 15 | ## [1.7.0](https://github.com/robbert229/jaeger-postgresql/compare/v1.6.0...v1.7.0) (2024-04-04) 16 | 17 | 18 | ### Features 19 | 20 | * added extra configuration methods ([#40](https://github.com/robbert229/jaeger-postgresql/issues/40)) ([947140b](https://github.com/robbert229/jaeger-postgresql/commit/947140b5888e8719dfd3d3e4c3af7833b15435ad)) 21 | 22 | ## [1.6.0](https://github.com/robbert229/jaeger-postgresql/compare/v1.5.1...v1.6.0) (2024-03-28) 23 | 24 | 25 | ### Features 26 | 27 | * added span gauge to prometheus ([#36](https://github.com/robbert229/jaeger-postgresql/issues/36)) ([f891462](https://github.com/robbert229/jaeger-postgresql/commit/f891462b44ca2b9f284c1149c14cecda7a9c2fc9)) 28 | 29 | ## [1.5.1](https://github.com/robbert229/jaeger-postgresql/compare/v1.5.0...v1.5.1) (2024-03-18) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * fixed issue with filtering not functioning for start_time ([#23](https://github.com/robbert229/jaeger-postgresql/issues/23)) ([46cdd8d](https://github.com/robbert229/jaeger-postgresql/commit/46cdd8d50a960be9c9dd0058131e91232db3eb43)) 35 | 36 | ## [1.5.0](https://github.com/robbert229/jaeger-postgresql/compare/v1.4.0...v1.5.0) (2024-03-17) 37 | 38 | 39 | ### Features 40 | 41 | * added indexes to make querying performant ([#17](https://github.com/robbert229/jaeger-postgresql/issues/17)) ([f92ef04](https://github.com/robbert229/jaeger-postgresql/commit/f92ef04bedb020f147ff72662082dc9e73c705af)) 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Are you enjoying using Jaeger-PostgreSQL and want to help? There's always 4 | room for improvement, so welcome aboard! 5 | 6 | Please take a moment to review this document in order to make the contribution 7 | process easy and effective for everyone involved. 8 | 9 | Following these guidelines helps to communicate that you respect the time of 10 | the developers managing and developing this open source project. In return, 11 | they should reciprocate that respect in addressing your issue or assessing 12 | patches and features. 13 | 14 | ## Using the issue tracker 15 | 16 | The [issue tracker](https://github.com/robbert229/jaeger-postgresql/issues) is 17 | the preferred channel for [bug reports](#bugs), [features requests](#features) 18 | and [submitting pull requests](#pull-requests). 19 | 20 | ## Bug reports 21 | 22 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 23 | Good bug reports are extremely helpful - thank you! 24 | 25 | Guidelines for bug reports: 26 | 27 | 1. **Use the GitHub issue search** — check if the issue has already been reported. 28 | 29 | 2. **Check if the issue has been fixed** — try to reproduce it using the latest `main` or development branch in the repository. 30 | 31 | 3. **Isolate the problem** — Have clear, and concise steps to reproduce the problem. 32 | 33 | 4. **Use the bug report template** — please fill in the template which appears when you open a new issue. 34 | 35 | A good bug report shouldn't leave others needing to chase you up for more information. Please try to be as detailed as possible in your report. What is your environment? What steps will reproduce the issue? What versions of Jaeger, OpenTelemetry, and OS 36 | experience the problem? What would you expect to be the outcome? All these details will help people to fix any potential bugs. 37 | 38 | Example: 39 | 40 | > ## Description 41 | > A clear and concise description of what the bug is. 42 | > 43 | > Any other information you want to share that is relevant to the issue being 44 | > reported. This might include the lines of code that you have identified as 45 | > causing the bug, and potential solutions (and your opinions on their 46 | > merits). 47 | > 48 | > ## Steps to reproduce 49 | > Steps to reproduce the behavior: 50 | > 51 | > 1. This is the first step 52 | > 2. This is the second step 53 | > 3. Further steps, etc. 54 | > 55 | > **Expected behavior** 56 | > A clear and concise description of what you expected to happen. 57 | > 58 | > **Screenshots** 59 | > If applicable, add screenshots to help explain your problem. 60 | > 61 | > ## Versions 62 | > 63 | > - Jaeger: 64 | > - OpenTelemetry Collector: 65 | > - OpenTelemetry SDK: 66 | > - Jaeger-PostgreSQL: 67 | 68 | 69 | 70 | ## Feature requests 71 | 72 | Feature requests are welcome. But take a moment to find out whether your idea fits with the scope and aims of the project. It's up to _you_ to make a strong case to convince the project's developers of the merits of this feature. Please provide as many details and as much context as possible. 73 | 74 | There is also a template for feature requests. Please make sure to use it. 75 | 76 | 77 | 78 | ## Pull requests 79 | 80 | Good pull requests - patches, improvements, new features - are a fantastic 81 | help. They should remain focused in scope and avoid containing unrelated 82 | commits. 83 | 84 | **Please ask first** before embarking on any significant pull request (e.g. 85 | implementing features, refactoring code), 86 | otherwise you risk spending a lot of time working on something that the 87 | project's developers might not want to merge into the project. 88 | 89 | Please adhere to the coding conventions used throughout a project (indentation, 90 | accurate comments, etc.) and any other requirements (such as test coverage). 91 | 92 | Since the `main` branch is what people actually use in production, we have a 93 | `dev` branch that unstable changes get merged into first. Only when we 94 | consider that stable we merge it into the `main` branch and release the 95 | changes for real. 96 | 97 | Adhering to the following process is the best way to get your work 98 | included in the project: 99 | 100 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) the project, clone your fork, and configure the remotes: 101 | 102 | ```bash 103 | # Clone your fork of the repo into the current directory 104 | git clone https://github.com//jaeger-postgresql.git 105 | # Navigate to the newly cloned directory 106 | cd jaeger-postgresql 107 | # Assign the original repo to a remote called "upstream" 108 | git remote add upstream https://github.com/robbert229/jaeger-postgresql.git 109 | ``` 110 | 111 | 2. If you cloned a while ago, get the latest changes from upstream: 112 | 113 | ```bash 114 | git checkout dev 115 | git pull upstream dev 116 | ``` 117 | 118 | 3. Create a new topic branch (off the `dev` branch) to contain your feature, change, or fix: 119 | 120 | ```bash 121 | git checkout -b 122 | ``` 123 | 124 | 4. Commit your changes in logical chunks. Please adhere to [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), or your code is unlikely be merged into the main project. Use Git's [interactive rebase](https://help.github.com/articles/about-git-rebase/) feature to tidy up your commits before making them public. 125 | 126 | 5. Locally merge (or rebase) the upstream dev branch into your topic branch: 127 | 128 | ```bash 129 | git pull [--rebase] upstream dev 130 | ``` 131 | 132 | 6. Push your topic branch up to your fork: 133 | 134 | ```bash 135 | git push origin 136 | ``` 137 | 138 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 139 | with a clear title and description. 140 | 141 | **IMPORTANT**: By submitting a patch, you agree to allow the project 142 | owners to license your work under the terms of the [MIT License](https://github.com/robbert229/jaeger-postgresql/blob/main/LICENSE.md). 143 | 144 | 145 | # Collaborating guidelines 146 | 147 | You can find the list of all contributors in [README.md](./README.md). 148 | 149 | There is one basic rules to ensure high quality of Jaeger-PostgreSQL: 150 | 151 | - Before merging, a PR requires at least one approval from the collaborators 152 | unless it's an architectural change, a large feature, etc. You are always 153 | welcome to discuss and propose improvements to this guideline. 154 | 155 | 156 | # Add yourself as a contributor 157 | 158 | This project follows the [All Contributors specification](https://allcontributors.org/). To add yourself to the table of contributors in the README file, please use the [bot](https://allcontributors.org/docs/en/bot/overview) or the [CLI](https://allcontributors.org/docs/en/cli/overview) as part of your PR. 159 | 160 | If you've already added yourself to the list and are making a new type of 161 | contribution, you can run it again and select the new contribution type. 162 | 163 | # Citations 164 | 165 | This document was lifted almost directly from [react-boilerplate](https://github.com/react-boilerplate/react-boilerplate/blob/master/CONTRIBUTING.md). Thank you for your wonderful documentation! 166 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 John Rowley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DBSTRING = postgres://postgres:password@localhost:5432/jaeger 2 | JAEGER_VERSION = 1.54.0 3 | VERSION ?= 'v1.5.0' # x-release-please-version 4 | 5 | .PHONY: publish 6 | publish: 7 | KO_DOCKER_REPO=ghcr.io/robbert229/jaeger-postgresql/ ko resolve --base-import-paths -t $(VERSION) -f ./charts/jaeger-postgresql/values-template.yaml > ./charts/jaeger-postgresql/values.yaml 8 | helm package ./charts/jaeger-postgresql --app-version $(VERSION) --version $(VERSION) --destination=./hack/charts/ 9 | helm push ./hack/charts/jaeger-postgresql-$(VERSION).tgz oci://ghcr.io/robbert229/jaeger-postgresql/charts 10 | 11 | # plugin-start starts Jaeger-PostgreSQL 12 | .PHONY: plugin-start 13 | plugin-start: 14 | go run ./cmd/jaeger-postgresql --database.url=$(DBSTRING) --log-level=debug 15 | 16 | # jaeger-start starts the all-in-one jaeger. 17 | .PHONY: jaeger-start 18 | jaeger-start: 19 | SPAN_STORAGE_TYPE='grpc-plugin' ./hack/jaeger-all-in-one --grpc-storage.server='127.0.0.1:12345' --query.enable-tracing=false 20 | 21 | .PHONY: install-all-in-one 22 | install-all-in-one: 23 | wget https://github.com/jaegertracing/jaeger/releases/download/v$(JAEGER_VERSION)/jaeger-$(JAEGER_VERSION)-linux-amd64.tar.gz -P ./hack/ 24 | tar -C ./hack --extract --file ./hack/jaeger-$(JAEGER_VERSION)-linux-amd64.tar.gz jaeger-$(JAEGER_VERSION)-linux-amd64/jaeger-all-in-one 25 | rm ./hack/jaeger-$(JAEGER_VERSION)-linux-amd64.tar.gz 26 | mv ./hack/jaeger-$(JAEGER_VERSION)-linux-amd64/jaeger-all-in-one ./hack/jaeger-all-in-one 27 | rmdir ./hack/jaeger-$(JAEGER_VERSION)-linux-amd64/ 28 | 29 | .PHONY: generate 30 | generate: 31 | sqlc generate 32 | 33 | # Install DB migration tool 34 | .PHONY: install-goose 35 | install-goose: 36 | @sh -c "which goose > /dev/null || go install github.com/pressly/goose/v3/cmd/goose@latest" 37 | 38 | .PHONY: install-sqlc 39 | install-sqlc: 40 | @sh -c "which sqlc > /dev/null || go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest" 41 | 42 | # Migrate SQL schema to latest version 43 | .PHONY: migrate 44 | migrate: install-goose 45 | GOOSE_DBSTRING=$(DBSTRING) GOOSE_DRIVER=postgres goose -dir ./internal/sql/migrations up 46 | 47 | # Redo SQL schema migration 48 | .PHONY: migrate-redo 49 | migrate-redo: install-goose 50 | GOOSE_DBSTRING=$(DBSTRING) GOOSE_DRIVER=postgres goose -dir ./internal/sql/migrations redo 51 | 52 | # Rollback SQL schema by one version 53 | .PHONY: migrate-down 54 | migrate-down: install-goose 55 | GOOSE_DBSTRING=$(DBSTRING) GOOSE_DRIVER=postgres goose -dir ./internal/sql/migrations down 56 | 57 | # Get SQL schema migration status 58 | .PHONY: migrate-status 59 | migrate-status: install-goose 60 | GOOSE_DBSTRING=$(DBSTRING) GOOSE_DRIVER=postgres goose -dir ./internal/sql/migrations status 61 | 62 | # tracegen-start starts a container that will produce test data spans. Useful for testing purposes. 63 | .PHONY: tracegen-start 64 | tracegen-start: 65 | docker run --rm --name jaeger-postgresql-tracegen --net=host \ 66 | jaegertracing/jaeger-tracegen:1.55 -traces=1000 67 | 68 | # tracegen-stop stops the jaeger-tracegen test data producer. 69 | .PHONY: tracegen-stop 70 | tracegen-stop: 71 | docker rm -f jaeger-postgresql-tracegen 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jaeger-PostgresQL 2 | 3 | ![GitHub License](https://img.shields.io/github/license/robbert229/jaeger-postgresql) 4 | ![GitHub Release](https://img.shields.io/github/v/release/robbert229/jaeger-postgresql) 5 | [![Star on GitHub](https://img.shields.io/github/stars/robbert229/jaeger-postgresql.svg?style=flat)](https://github.com/robbert229/jaeger-postresql/stargazers) 6 | ![GitHub contributors from allcontributors.org](https://img.shields.io/github/all-contributors/robbert229/jaeger-postgresql) 7 | ![Postgres](https://img.shields.io/badge/postgres-%23316192.svg?style=flat&logo=postgresql&logoColor=white) 8 | ![Go](https://img.shields.io/badge/go-%2300ADD8.svg?style=flat&logo=go&logoColor=white) 9 | ![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=flat&logo=kubernetes&logoColor=white) 10 | 11 | PostgreSQL is a great general purpose. Jaeger-PostgreSQL is intended to allow jaeger 12 | to use postgresql as its backing storage solution for projects with low traffic. 13 | 14 | ## Install 15 | 16 | Installation is done through the use of a helm chart. 17 | 18 | 19 | ``` 20 | helm install myrelease oci://ghcr.io/robbert229/jaeger-postgresql/charts/jaeger-postgresql \ 21 | --version v1.7.0 \ 22 | --set database.url='postgresql://postgres:password@database:5432/jaeger' 23 | ``` 24 | 25 | 26 | ``` 27 | # database connection options 28 | database: 29 | # url to the database 30 | url: "postgresql://postgres:password@database:5432/jaeger" 31 | 32 | # the maximum number of database connections 33 | maxConns: 10 34 | 35 | # configuration options for the cleaner 36 | cleaner: 37 | # when true the cleaner will ensure that spans older than a set age will 38 | # be deleted. 39 | enabled: true 40 | 41 | # go duration formatted duration indicating the maximum age of a span 42 | # before the cleaner removes it. 43 | maxSpanAge: "24h" 44 | ``` 45 | 46 | ## Usage 47 | 48 | The Helm chart will deploy a service with the same name as the helm release. 49 | The configuration of Jaeger to use jaeger-postgresql depends on how you 50 | deployed jaeger. Adding the following argument to the jaeger's services, along 51 | with the acompanying environment variables to your jaeger services will 52 | configure jaeger to use Jaeger-PostgresQL for storage 53 | 54 | `--grpc-storage.server=jaeger-postgresql:12345` 55 | 56 | `SPAN_STORAGE_TYPE="grpc-plugin"` 57 | 58 | The official jaeger documentation is the best place to look for detailed instructions on using a external storage plugin. https://www.jaegertracing.io/docs/1.55/deployment/#storage-plugin 59 | 60 | ## Contributors ✨ 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
Jozef Slezak
Jozef Slezak

💻
John Rowley
John Rowley

💻
Nicolas De los Santos
Nicolas De los Santos

💻
74 | 75 | 76 | 77 | 78 | 79 | 80 | ## Legacy 81 | 82 | This project started out as a simple fork of [Jozef Slezak's plugin of the same name](jozef-slezak/jaeger-postgresql), but was eventually completely rewritten. 83 | -------------------------------------------------------------------------------- /charts/jaeger-postgresql/.gitignore: -------------------------------------------------------------------------------- 1 | values.yaml -------------------------------------------------------------------------------- /charts/jaeger-postgresql/.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 | -------------------------------------------------------------------------------- /charts/jaeger-postgresql/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: jaeger-postgresql 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: v0.0.0 19 | -------------------------------------------------------------------------------- /charts/jaeger-postgresql/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "jaeger-postgresql.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-postgresql.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-postgresql.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "jaeger-postgresql.labels" -}} 37 | helm.sh/chart: {{ include "jaeger-postgresql.chart" . }} 38 | {{ include "jaeger-postgresql.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-postgresql.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "jaeger-postgresql.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Return the target Kubernetes version 55 | */}} 56 | {{- define "common.capabilities.kubeVersion" -}} 57 | {{- if .Values.global }} 58 | {{- if .Values.global.kubeVersion }} 59 | {{- .Values.global.kubeVersion -}} 60 | {{- else }} 61 | {{- default .Capabilities.KubeVersion.Version .Values.kubeVersion -}} 62 | {{- end -}} 63 | {{- else }} 64 | {{- default .Capabilities.KubeVersion.Version .Values.kubeVersion -}} 65 | {{- end -}} 66 | {{- end -}} 67 | 68 | {{/* 69 | Return the appropriate apiVersion for cronjob. 70 | */}} 71 | {{- define "common.capabilities.cronjob.apiVersion" -}} 72 | {{- if semverCompare "<1.21-0" (include "common.capabilities.kubeVersion" .) -}} 73 | {{- print "batch/v1beta1" -}} 74 | {{- else -}} 75 | {{- print "batch/v1" -}} 76 | {{- end -}} 77 | {{- end -}} -------------------------------------------------------------------------------- /charts/jaeger-postgresql/templates/cleaner.yaml: -------------------------------------------------------------------------------- 1 | {{ if .Values.cleaner.enabled }} 2 | 3 | apiVersion: {{ include "common.capabilities.cronjob.apiVersion" . }} 4 | kind: CronJob 5 | metadata: 6 | name: {{ include "jaeger-postgresql.fullname" . }}-cleaner 7 | labels: 8 | {{- include "jaeger-postgresql.labels" . | nindent 4 }} 9 | component: cleaner 10 | spec: 11 | schedule: "0 0 * * *" 12 | concurrencyPolicy: "Replace" 13 | jobTemplate: 14 | metadata: 15 | {{- with .Values.cleaner.podAnnotations }} 16 | annotations: 17 | {{- toYaml . | nindent 8 }} 18 | {{- end }} 19 | labels: 20 | {{- include "jaeger-postgresql.labels" . | nindent 8 }} 21 | {{- with .Values.cleaner.podLabels }} 22 | {{- toYaml . | nindent 8 }} 23 | {{- end }} 24 | component: cleaner 25 | spec: 26 | template: 27 | metadata: 28 | {{- with .Values.cleaner.podAnnotations }} 29 | annotations: 30 | {{- toYaml . | nindent 12 }} 31 | {{- end }} 32 | labels: 33 | {{- include "jaeger-postgresql.labels" . | nindent 12 }} 34 | {{- with .Values.cleaner.podLabels }} 35 | {{- toYaml . | nindent 12 }} 36 | {{- end }} 37 | component: cleaner 38 | spec: 39 | restartPolicy: OnFailure 40 | {{- with .Values.cleaner.imagePullSecrets }} 41 | imagePullSecrets: 42 | {{- toYaml . | nindent 12 }} 43 | {{- end }} 44 | securityContext: 45 | {{- toYaml .Values.cleaner.podSecurityContext | nindent 12 }} 46 | containers: 47 | - name: cleaner 48 | args: 49 | - "--log-level" 50 | - "{{ .Values.cleaner.logLevel }}" 51 | - "--database.url" 52 | - "{{ .Values.database.url }}" 53 | - "--database.max-conns" 54 | - "{{ .Values.database.maxConns}}" 55 | - "--max-span-age" 56 | - "{{ .Values.cleaner.maxSpanAge }}" 57 | securityContext: 58 | {{- toYaml .Values.cleaner.securityContext | nindent 16 }} 59 | image: "{{ .Values.cleaner.image }}" 60 | imagePullPolicy: {{ .Values.cleaner.imagePullPolicy}} 61 | resources: 62 | {{- toYaml .Values.cleaner.resources | nindent 16 }} 63 | {{- with .Values.cleaner.volumeMounts }} 64 | volumeMounts: 65 | {{- toYaml . | nindent 16 }} 66 | {{- end }} 67 | {{- with .Values.cleaner.volumes }} 68 | volumes: 69 | {{- toYaml . | nindent 12 }} 70 | {{- end }} 71 | {{- with .Values.cleaner.nodeSelector }} 72 | nodeSelector: 73 | {{- toYaml . | nindent 12 }} 74 | {{- end }} 75 | {{- with .Values.cleaner.affinity }} 76 | affinity: 77 | {{- toYaml . | nindent 12 }} 78 | {{- end }} 79 | {{- with .Values.cleaner.tolerations }} 80 | tolerations: 81 | {{- toYaml . | nindent 12 }} 82 | {{- end }} 83 | {{ end }} -------------------------------------------------------------------------------- /charts/jaeger-postgresql/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "jaeger-postgresql.fullname" . }} 5 | labels: 6 | {{- include "jaeger-postgresql.labels" . | nindent 4 }} 7 | component: service 8 | spec: 9 | replicas: {{ .Values.service.replicaCount }} 10 | selector: 11 | matchLabels: 12 | {{- include "jaeger-postgresql.selectorLabels" . | nindent 6 }} 13 | component: service 14 | template: 15 | metadata: 16 | {{- with .Values.service.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "jaeger-postgresql.labels" . | nindent 8 }} 22 | {{- with .Values.service.podLabels }} 23 | {{- toYaml . | nindent 8 }} 24 | {{- end }} 25 | component: service 26 | spec: 27 | {{- with .Values.service.imagePullSecrets }} 28 | imagePullSecrets: 29 | {{- toYaml . | nindent 8 }} 30 | {{- end }} 31 | securityContext: 32 | {{- toYaml .Values.service.podSecurityContext | nindent 8 }} 33 | containers: 34 | - name: jaeger-postgresql 35 | env: 36 | {{- range $key, $value := .Values.extraEnvs }} 37 | - name: {{ $key }} 38 | value: {{ $value | quote }} 39 | {{- end }} 40 | - name: JAEGER_POSTGRESQL_LOG_LEVEL 41 | value: {{ .Values.service.logLevel | quote }} 42 | - name: JAEGER_POSTGRESQL_DATABASE_MAX_CONNS 43 | value: {{ .Values.database.maxConns | quote }} 44 | - name: JAEGER_POSTGRESQL_DATABASE_URL 45 | {{ with .Values.database.urlFromSecret }} 46 | valueFrom: 47 | secretKeyRef: 48 | name: {{ .name }} 49 | key: {{ .key }} 50 | {{ else }} 51 | value: {{ .Values.database.url | quote }} 52 | {{ end }} 53 | - name: JAEGER_POSTGRESQL_GRPC_SERVER_HOST_PORT 54 | value: "0.0.0.0:12345" 55 | - name: JAEGER_POSTGRESQL_ADMIN_HTTP_HOST_PORT 56 | value: "0.0.0.0:12346" 57 | securityContext: 58 | {{- toYaml .Values.service.securityContext | nindent 12 }} 59 | image: "{{ .Values.service.image }}" 60 | imagePullPolicy: {{ .Values.service.imagePullPolicy }} 61 | ports: 62 | - name: grpc 63 | containerPort: 12345 64 | protocol: TCP 65 | - name: metrics 66 | containerPort: 12346 67 | protocol: TCP 68 | livenessProbe: 69 | httpGet: 70 | path: / 71 | port: metrics 72 | readinessProbe: 73 | httpGet: 74 | path: / 75 | port: metrics 76 | resources: 77 | {{- toYaml .Values.service.resources | nindent 12 }} 78 | {{- with .Values.service.volumeMounts }} 79 | volumeMounts: 80 | {{- toYaml . | nindent 12 }} 81 | {{- end }} 82 | {{- with .Values.service.volumes }} 83 | volumes: 84 | {{- toYaml . | nindent 8 }} 85 | {{- end }} 86 | {{- with .Values.service.nodeSelector }} 87 | nodeSelector: 88 | {{- toYaml . | nindent 8 }} 89 | {{- end }} 90 | {{- with .Values.service.affinity }} 91 | affinity: 92 | {{- toYaml . | nindent 8 }} 93 | {{- end }} 94 | {{- with .Values.service.tolerations }} 95 | tolerations: 96 | {{- toYaml . | nindent 8 }} 97 | {{- end }} 98 | -------------------------------------------------------------------------------- /charts/jaeger-postgresql/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "jaeger-postgresql.fullname" . }} 5 | labels: 6 | {{- include "jaeger-postgresql.labels" . | nindent 4 }} 7 | annotations: 8 | prometheus.io/scrape: "true" 9 | prometheus.io/scheme: "http" 10 | prometheus.io/path: "/metrics" 11 | prometheus.io/port: "12346" 12 | spec: 13 | type: ClusterIP 14 | ports: 15 | - port: 12345 16 | targetPort: grpc 17 | protocol: TCP 18 | name: grpc 19 | - port: 12346 20 | targetPort: metrics 21 | protocol: TCP 22 | name: metrics 23 | selector: 24 | {{- include "jaeger-postgresql.selectorLabels" . | nindent 4 }} 25 | -------------------------------------------------------------------------------- /charts/jaeger-postgresql/values-template.yaml: -------------------------------------------------------------------------------- 1 | database: 2 | url: "postgresql://postgres:password@localhost:5432/jaeger" 3 | maxConns: 10 4 | 5 | # -- (object) Source database url from a secret 6 | urlFromSecret: 7 | # name of secret 8 | # name: "" 9 | # key within secret containing url 10 | # key: "" 11 | 12 | service: 13 | logLevel: "info" 14 | 15 | replicaCount: 2 16 | 17 | image: ko://github.com/robbert229/jaeger-postgresql/cmd/jaeger-postgresql 18 | imagePullPolicy: IfNotPresent 19 | 20 | imagePullSecrets: [] 21 | nameOverride: "" 22 | fullnameOverride: "" 23 | 24 | podAnnotations: {} 25 | podLabels: {} 26 | 27 | podSecurityContext: 28 | fsGroup: 2000 29 | 30 | securityContext: 31 | capabilities: 32 | drop: 33 | - ALL 34 | readOnlyRootFilesystem: true 35 | runAsNonRoot: true 36 | runAsUser: 1000 37 | 38 | resources: {} 39 | # limits: 40 | # cpu: 100m 41 | # memory: 128Mi 42 | # requests: 43 | # cpu: 100m 44 | # memory: 128Mi 45 | 46 | # Additional volumes on the output Deployment definition. 47 | volumes: [] 48 | # - name: foo 49 | # secret: 50 | # secretName: mysecret 51 | # optional: false 52 | 53 | # Additional volumeMounts on the output Deployment definition. 54 | volumeMounts: [] 55 | # - name: foo 56 | # mountPath: "/etc/foo" 57 | # readOnly: true 58 | 59 | nodeSelector: {} 60 | 61 | tolerations: [] 62 | 63 | affinity: {} 64 | 65 | cleaner: 66 | enabled: true 67 | 68 | maxSpanAge: 24h 69 | logLevel: "debug" 70 | 71 | image: ko://github.com/robbert229/jaeger-postgresql/cmd/jaeger-postgresql-cleaner 72 | imagePullPolicy: IfNotPresent 73 | 74 | imagePullSecrets: [] 75 | nameOverride: "" 76 | fullnameOverride: "" 77 | 78 | podAnnotations: {} 79 | podLabels: {} 80 | 81 | podSecurityContext: 82 | fsGroup: 2000 83 | 84 | securityContext: 85 | capabilities: 86 | drop: 87 | - ALL 88 | readOnlyRootFilesystem: true 89 | runAsNonRoot: true 90 | runAsUser: 1000 91 | 92 | resources: {} 93 | # limits: 94 | # cpu: 100m 95 | # memory: 128Mi 96 | # requests: 97 | # cpu: 100m 98 | # memory: 128Mi 99 | 100 | # Additional volumes on the output Deployment definition. 101 | volumes: [] 102 | # - name: foo 103 | # secret: 104 | # secretName: mysecret 105 | # optional: false 106 | 107 | # Additional volumeMounts on the output Deployment definition. 108 | volumeMounts: [] 109 | # - name: foo 110 | # mountPath: "/etc/foo" 111 | # readOnly: true 112 | 113 | nodeSelector: {} 114 | 115 | tolerations: [] 116 | 117 | affinity: {} 118 | 119 | extraEnvs: [] -------------------------------------------------------------------------------- /cmd/jaeger-postgresql-cleaner/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/fs" 7 | "log/slog" 8 | "strings" 9 | "time" 10 | 11 | "github.com/jackc/pgx/v5/pgtype" 12 | "github.com/jackc/pgx/v5/pgxpool" 13 | "github.com/robbert229/jaeger-postgresql/internal/logger" 14 | "github.com/robbert229/jaeger-postgresql/internal/sql" 15 | "github.com/spf13/pflag" 16 | "github.com/spf13/viper" 17 | "go.uber.org/fx" 18 | "go.uber.org/fx/fxevent" 19 | ) 20 | 21 | // ProvideLogger returns a function that provides a logger 22 | func ProvideLogger() any { 23 | return func(cfg Config) (*slog.Logger, error) { 24 | return logger.New(&cfg.LogLevel) 25 | } 26 | } 27 | 28 | // ProvidePgxPool returns a function that provides a pgx pool 29 | func ProvidePgxPool() any { 30 | return func(cfg Config, logger *slog.Logger, lc fx.Lifecycle) (*pgxpool.Pool, error) { 31 | databaseURL := cfg.Database.URL 32 | if databaseURL == "" { 33 | return nil, fmt.Errorf("invalid database url") 34 | } 35 | 36 | err := sql.Migrate(logger, databaseURL) 37 | if err != nil { 38 | return nil, fmt.Errorf("failed to migrate database: %w", err) 39 | } 40 | 41 | pgxconfig, err := pgxpool.ParseConfig(databaseURL) 42 | if err != nil { 43 | return nil, fmt.Errorf("failed to parse database url") 44 | } 45 | 46 | // handle max conns 47 | { 48 | var maxConns int32 49 | if cfg.Database.MaxConns == 0 { 50 | maxConns = 20 51 | } else { 52 | maxConns = int32(cfg.Database.MaxConns) 53 | } 54 | 55 | pgxconfig.MaxConns = maxConns 56 | } 57 | 58 | // handle timeout duration 59 | connectTimeoutDuration := time.Second * 10 60 | pgxconfig.ConnConfig.ConnectTimeout = connectTimeoutDuration 61 | 62 | ctx, cancelFn := context.WithTimeout(context.Background(), connectTimeoutDuration) 63 | defer cancelFn() 64 | 65 | pool, err := pgxpool.NewWithConfig(ctx, pgxconfig) 66 | if err != nil { 67 | return nil, fmt.Errorf("failed to connect to the postgres database: %w", err) 68 | } 69 | 70 | logger.Info("connected to postgres") 71 | 72 | lc.Append(fx.Hook{ 73 | OnStop: func(ctx context.Context) error { 74 | pool.Close() 75 | return nil 76 | }, 77 | }) 78 | 79 | return pool, nil 80 | } 81 | } 82 | 83 | // clean purges the old roles from the database 84 | func clean(ctx context.Context, pool *pgxpool.Pool, maxAge time.Duration) (int64, error) { 85 | q := sql.New(pool) 86 | result, err := q.CleanSpans(ctx, pgtype.Timestamp{Time: time.Now().Add(-1 * maxAge), Valid: true}) 87 | if err != nil { 88 | return 0, err 89 | } 90 | 91 | return result, nil 92 | } 93 | 94 | type Config struct { 95 | Database struct { 96 | URL string `mapstructure:"url"` 97 | MaxConns int `mapstructure:"max-conns"` 98 | } `mapstructure:"database"` 99 | 100 | LogLevel string `mapstructure:"log-level"` 101 | 102 | MaxSpanAge time.Duration `mapstructure:"max-span-age"` 103 | } 104 | 105 | func ProvideConfig() func() (Config, error) { 106 | return func() (Config, error) { 107 | pflag.String("database.url", "", "the postgres connection url to use to connect to the database") 108 | pflag.Int("database.max-conns", 20, "Max number of database connections of which the plugin will try to maintain at any given time") 109 | pflag.String("log-level", "warn", "Minimal allowed log level") 110 | pflag.Duration("max-span-age", time.Hour*24, "Maximum age of a span before it will be cleaned") 111 | 112 | v := viper.New() 113 | v.SetEnvPrefix("JAEGER_POSTGRESQL") 114 | v.AutomaticEnv() 115 | v.SetConfigFile("jaeger-postgresql") 116 | v.SetConfigType("yaml") 117 | v.AddConfigPath(".") 118 | v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) 119 | pflag.Parse() 120 | v.BindPFlags(pflag.CommandLine) 121 | 122 | var cfg Config 123 | if err := v.ReadInConfig(); err != nil { 124 | _, ok := err.(*fs.PathError) 125 | _, ok2 := err.(viper.ConfigFileNotFoundError) 126 | 127 | if !ok && !ok2 { 128 | return cfg, fmt.Errorf("failed to read in config: %w", err) 129 | } 130 | } 131 | 132 | err := v.Unmarshal(&cfg) 133 | if err != nil { 134 | return cfg, fmt.Errorf("failed to decode configuration: %w", err) 135 | } 136 | 137 | return cfg, nil 138 | } 139 | } 140 | 141 | func main() { 142 | fx.New( 143 | fx.WithLogger(func(logger *slog.Logger) fxevent.Logger { 144 | return &fxevent.SlogLogger{Logger: logger.With("component", "uber/fx")} 145 | }), 146 | fx.Provide( 147 | ProvideConfig(), 148 | ProvideLogger(), 149 | ProvidePgxPool(), 150 | ), 151 | fx.Invoke(func(cfg Config, pool *pgxpool.Pool, lc fx.Lifecycle, logger *slog.Logger, stopper fx.Shutdowner) error { 152 | go func(ctx context.Context) { 153 | ctx, cancelFn := context.WithTimeout(ctx, time.Minute) 154 | defer cancelFn() 155 | 156 | count, err := clean(ctx, pool, cfg.MaxSpanAge) 157 | if err != nil { 158 | logger.Error("failed to clean database", "err", err) 159 | stopper.Shutdown(fx.ExitCode(1)) 160 | return 161 | } 162 | 163 | logger.Info("successfully cleaned database", "spans", count) 164 | stopper.Shutdown(fx.ExitCode(0)) 165 | }(context.Background()) 166 | return nil 167 | }), 168 | ).Run() 169 | } 170 | -------------------------------------------------------------------------------- /cmd/jaeger-postgresql/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/fs" 7 | "log/slog" 8 | "net" 9 | "net/http" 10 | "strings" 11 | "time" 12 | 13 | "github.com/jackc/pgx/v5/pgxpool" 14 | "github.com/jaegertracing/jaeger/plugin/storage/grpc/shared" 15 | "github.com/jaegertracing/jaeger/storage/dependencystore" 16 | "github.com/jaegertracing/jaeger/storage/spanstore" 17 | "github.com/prometheus/client_golang/prometheus" 18 | "github.com/prometheus/client_golang/prometheus/promauto" 19 | "github.com/prometheus/client_golang/prometheus/promhttp" 20 | "github.com/robbert229/jaeger-postgresql/internal/logger" 21 | "github.com/robbert229/jaeger-postgresql/internal/sql" 22 | "github.com/robbert229/jaeger-postgresql/internal/store" 23 | "github.com/spf13/pflag" 24 | "github.com/spf13/viper" 25 | "go.uber.org/fx" 26 | "go.uber.org/fx/fxevent" 27 | "google.golang.org/grpc" 28 | ) 29 | 30 | // ProvideLogger returns a function that provides a logger 31 | func ProvideLogger() any { 32 | return func(cfg Config) (*slog.Logger, error) { 33 | return logger.New(&cfg.LogLevel) 34 | } 35 | } 36 | 37 | // ProvidePgxPool returns a function that provides a pgx pool 38 | func ProvidePgxPool() any { 39 | return func(cfg Config, logger *slog.Logger, lc fx.Lifecycle) (*pgxpool.Pool, error) { 40 | databaseURL := cfg.Database.URL 41 | if databaseURL == "" { 42 | return nil, fmt.Errorf("invalid database url") 43 | } 44 | 45 | err := sql.Migrate(logger, databaseURL) 46 | if err != nil { 47 | return nil, fmt.Errorf("failed to migrate database: %w", err) 48 | } 49 | 50 | pgxconfig, err := pgxpool.ParseConfig(databaseURL) 51 | if err != nil { 52 | return nil, fmt.Errorf("failed to parse database url") 53 | } 54 | 55 | // handle max conns 56 | { 57 | var maxConns int32 58 | if cfg.Database.MaxConns == 0 { 59 | maxConns = 20 60 | } else { 61 | maxConns = int32(cfg.Database.MaxConns) 62 | } 63 | 64 | pgxconfig.MaxConns = maxConns 65 | } 66 | 67 | // handle timeout duration 68 | connectTimeoutDuration := time.Second * 10 69 | pgxconfig.ConnConfig.ConnectTimeout = connectTimeoutDuration 70 | 71 | ctx, cancelFn := context.WithTimeout(context.Background(), connectTimeoutDuration) 72 | defer cancelFn() 73 | 74 | pool, err := pgxpool.NewWithConfig(ctx, pgxconfig) 75 | if err != nil { 76 | return nil, fmt.Errorf("failed to connect to the postgres database: %w", err) 77 | } 78 | 79 | logger.Info("connected to postgres") 80 | 81 | lc.Append(fx.Hook{ 82 | OnStop: func(ctx context.Context) error { 83 | pool.Close() 84 | return nil 85 | }, 86 | }) 87 | 88 | return pool, nil 89 | } 90 | } 91 | 92 | // ProvideSpanStoreReader returns a function that provides a spanstore reader. 93 | func ProvideSpanStoreReader() any { 94 | return func(pool *pgxpool.Pool, logger *slog.Logger) spanstore.Reader { 95 | q := sql.New(pool) 96 | return store.NewInstrumentedReader(store.NewReader(q, logger), logger) 97 | } 98 | } 99 | 100 | // ProvideSpanStoreWriter returns a function that provides a spanstore writer 101 | func ProvideSpanStoreWriter() any { 102 | return func(pool *pgxpool.Pool, logger *slog.Logger) spanstore.Writer { 103 | q := sql.New(pool) 104 | return store.NewInstrumentedWriter(store.NewWriter(q, logger), logger) 105 | } 106 | } 107 | 108 | // ProvideDependencyStoreReader provides a dependencystore reader 109 | func ProvideDependencyStoreReader() any { 110 | return func(pool *pgxpool.Pool, logger *slog.Logger) dependencystore.Reader { 111 | q := sql.New(pool) 112 | return store.NewReader(q, logger) 113 | } 114 | } 115 | 116 | // ProvideHandler provides a grpc handler. 117 | func ProvideHandler() any { 118 | return func(reader spanstore.Reader, writer spanstore.Writer, dependencyReader dependencystore.Reader) *shared.GRPCHandler { 119 | handler := shared.NewGRPCHandler(&shared.GRPCHandlerStorageImpl{ 120 | SpanReader: func() spanstore.Reader { return reader }, 121 | SpanWriter: func() spanstore.Writer { return writer }, 122 | DependencyReader: func() dependencystore.Reader { return dependencyReader }, 123 | ArchiveSpanReader: func() spanstore.Reader { return nil }, 124 | ArchiveSpanWriter: func() spanstore.Writer { return nil }, 125 | StreamingSpanWriter: func() spanstore.Writer { return nil }, 126 | }) 127 | 128 | return handler 129 | } 130 | } 131 | 132 | // ProvideGRPCServer provides a grpc server. 133 | func ProvideGRPCServer() any { 134 | return func(lc fx.Lifecycle, cfg Config, logger *slog.Logger) (*grpc.Server, error) { 135 | srv := grpc.NewServer() 136 | 137 | if cfg.GRPCServer.HostPort == "" { 138 | return nil, fmt.Errorf("invalid grpc-server.host-port given: %s", cfg.GRPCServer.HostPort) 139 | } 140 | 141 | lis, err := net.Listen("tcp", cfg.GRPCServer.HostPort) 142 | if err != nil { 143 | return nil, fmt.Errorf("failed to listen: %w", err) 144 | } 145 | 146 | logger.Info("grpc server started", "addr", cfg.GRPCServer.HostPort) 147 | 148 | lc.Append(fx.StartStopHook( 149 | func(ctx context.Context) error { 150 | go srv.Serve(lis) 151 | return nil 152 | }, 153 | 154 | func(ctx context.Context) error { 155 | srv.GracefulStop() 156 | return lis.Close() 157 | }, 158 | )) 159 | 160 | return srv, nil 161 | } 162 | } 163 | 164 | // ProvideAdminServer provides the admin http server. 165 | func ProvideAdminServer() any { 166 | return func(lc fx.Lifecycle, cfg Config, logger *slog.Logger) (*http.ServeMux, error) { 167 | mux := http.NewServeMux() 168 | 169 | srv := http.Server{ 170 | Handler: mux, 171 | } 172 | 173 | if cfg.Admin.HTTP.HostPort == "" { 174 | return nil, fmt.Errorf("invalid admin.http.host-port given: %s", cfg.Admin.HTTP.HostPort) 175 | } 176 | 177 | lis, err := net.Listen("tcp", cfg.Admin.HTTP.HostPort) 178 | if err != nil { 179 | return nil, fmt.Errorf("failed to listen: %w", err) 180 | } 181 | 182 | logger.Info("admin server started", "addr", lis.Addr()) 183 | 184 | lc.Append(fx.StartStopHook( 185 | func(ctx context.Context) error { 186 | go srv.Serve(lis) 187 | return nil 188 | }, 189 | 190 | func(ctx context.Context) error { 191 | return srv.Shutdown(ctx) 192 | }, 193 | )) 194 | 195 | return mux, nil 196 | } 197 | } 198 | 199 | var ( 200 | spansTableDiskSizeGuage = promauto.NewGauge(prometheus.GaugeOpts{ 201 | Namespace: "jaeger_postgresql", 202 | Name: "spans_table_bytes", 203 | Help: "The size of the spans table in bytes", 204 | }) 205 | 206 | spansGuage = promauto.NewGauge(prometheus.GaugeOpts{ 207 | Namespace: "jaeger_postgresql", 208 | Name: "spans_count", 209 | Help: "The number of spans", 210 | }) 211 | ) 212 | 213 | // Config is the configuration struct for the jaeger-postgresql service. 214 | type Config struct { 215 | Database struct { 216 | URL string `mapstructure:"url"` 217 | MaxConns int `mapstructure:"max-conns"` 218 | } `mapstructure:"database"` 219 | 220 | LogLevel string `mapstructure:"log-level"` 221 | 222 | GRPCServer struct { 223 | HostPort string `mapstructure:"host-port"` 224 | } `mapstructure:"grpc-server"` 225 | 226 | Admin struct { 227 | HTTP struct { 228 | HostPort string `mapstructure:"host-port"` 229 | } 230 | } 231 | } 232 | 233 | func ProvideConfig() func() (Config, error) { 234 | return func() (Config, error) { 235 | pflag.String("database.url", "", "the postgres connection url to use to connect to the database") 236 | pflag.Int("database.max-conns", 20, "Max number of database connections of which the plugin will try to maintain at any given time") 237 | pflag.String("log-level", "warn", "Minimal allowed log level") 238 | pflag.String("grpc-server.host-port", ":12345", "the host:port (eg 127.0.0.1:12345 or :12345) of the storage provider's gRPC server") 239 | pflag.String("admin.http.host-port", ":12346", "The host:port (e.g. 127.0.0.1:12346 or :12346) for the admin server, including health check, /metrics, etc.") 240 | 241 | v := viper.New() 242 | v.SetEnvPrefix("JAEGER_POSTGRESQL") 243 | v.AutomaticEnv() 244 | v.SetConfigFile("jaeger-postgresql") 245 | v.SetConfigType("yaml") 246 | v.AddConfigPath(".") 247 | v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) 248 | pflag.Parse() 249 | v.BindPFlags(pflag.CommandLine) 250 | 251 | var cfg Config 252 | if err := v.ReadInConfig(); err != nil { 253 | _, ok := err.(*fs.PathError) 254 | _, ok2 := err.(viper.ConfigFileNotFoundError) 255 | 256 | if !ok && !ok2 { 257 | return cfg, fmt.Errorf("failed to read in config: %w", err) 258 | } 259 | } 260 | 261 | err := v.Unmarshal(&cfg) 262 | if err != nil { 263 | return cfg, fmt.Errorf("failed to decode configuration: %w", err) 264 | } 265 | 266 | return cfg, nil 267 | } 268 | } 269 | 270 | func main() { 271 | fx.New( 272 | fx.WithLogger(func(logger *slog.Logger) fxevent.Logger { 273 | return &fxevent.SlogLogger{Logger: logger.With("component", "uber/fx")} 274 | }), 275 | fx.Provide( 276 | ProvideConfig(), 277 | ProvideLogger(), 278 | ProvidePgxPool(), 279 | ProvideSpanStoreReader(), 280 | ProvideSpanStoreWriter(), 281 | ProvideDependencyStoreReader(), 282 | ProvideHandler(), 283 | ProvideGRPCServer(), 284 | ProvideAdminServer(), 285 | ), 286 | fx.Invoke(func(srv *grpc.Server, handler *shared.GRPCHandler) error { 287 | return handler.Register(srv) 288 | }), 289 | fx.Invoke(func(conn *pgxpool.Pool, logger *slog.Logger, lc fx.Lifecycle) { 290 | ctx, cancelFn := context.WithCancel(context.Background()) 291 | lc.Append(fx.StopHook(cancelFn)) 292 | 293 | go func() { 294 | q := sql.New(conn) 295 | ticker := time.NewTicker(time.Second * 5) 296 | defer ticker.Stop() 297 | 298 | for { 299 | select { 300 | case <-ctx.Done(): 301 | return 302 | case <-ticker.C: 303 | byteCount, err := q.GetSpansDiskSize(ctx) 304 | if err != nil { 305 | logger.Error("failed to query for disk size", "err", err) 306 | continue 307 | } else { 308 | spansTableDiskSizeGuage.Set(float64(byteCount)) 309 | } 310 | 311 | count, err := q.GetSpansCount(ctx) 312 | if err != nil { 313 | logger.Error("failed to query for span count", "err", err) 314 | continue 315 | } else { 316 | spansGuage.Set(float64(count)) 317 | } 318 | 319 | } 320 | } 321 | }() 322 | }), 323 | fx.Invoke(func(mux *http.ServeMux, conn *pgxpool.Pool) { 324 | mux.Handle("/metrics", promhttp.Handler()) 325 | mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 326 | ctx, cancelFn := context.WithTimeout(r.Context(), time.Second*5) 327 | defer cancelFn() 328 | 329 | err := conn.Ping(ctx) 330 | if err != nil { 331 | w.WriteHeader(http.StatusInternalServerError) 332 | } 333 | 334 | w.WriteHeader(http.StatusOK) 335 | })) 336 | }), 337 | ).Run() 338 | } 339 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | postgres: 4 | container_name: postgres 5 | image: postgres:15 6 | environment: 7 | - POSTGRES_PASSWORD=password 8 | restart: always 9 | ports: 10 | - "5432:5432" 11 | volumes: 12 | - pgdata:/var/lib/postgresql/data 13 | volumes: 14 | pgdata: 15 | external: true 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/robbert229/jaeger-postgresql 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.5 6 | 7 | require ( 8 | github.com/jackc/pgx/v5 v5.7.1 9 | github.com/jaegertracing/jaeger v1.55.0 10 | github.com/pressly/goose/v3 v3.24.1 11 | github.com/prometheus/client_golang v1.19.0 12 | github.com/spf13/pflag v1.0.5 13 | github.com/spf13/viper v1.18.2 14 | github.com/stretchr/testify v1.10.0 15 | github.com/testcontainers/testcontainers-go v0.32.0 16 | github.com/testcontainers/testcontainers-go/modules/postgres v0.32.0 17 | go.opentelemetry.io/otel/trace v1.34.0 18 | go.uber.org/fx v1.21.0 19 | google.golang.org/grpc v1.70.0 20 | ) 21 | 22 | require ( 23 | dario.cat/mergo v1.0.0 // indirect 24 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 25 | github.com/Microsoft/go-winio v0.6.2 // indirect 26 | github.com/Microsoft/hcsshim v0.12.1 // indirect 27 | github.com/beorn7/perks v1.0.1 // indirect 28 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 29 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 30 | github.com/containerd/containerd v1.7.18 // indirect 31 | github.com/containerd/errdefs v0.1.0 // indirect 32 | github.com/containerd/log v0.1.0 // indirect 33 | github.com/cpuguy83/dockercfg v0.3.1 // indirect 34 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 35 | github.com/distribution/reference v0.6.0 // indirect 36 | github.com/docker/docker v27.1.1+incompatible // indirect 37 | github.com/docker/go-connections v0.5.0 // indirect 38 | github.com/docker/go-units v0.5.0 // indirect 39 | github.com/fatih/color v1.16.0 // indirect 40 | github.com/felixge/httpsnoop v1.0.4 // indirect 41 | github.com/fsnotify/fsnotify v1.7.0 // indirect 42 | github.com/go-logr/logr v1.4.2 // indirect 43 | github.com/go-logr/stdr v1.2.2 // indirect 44 | github.com/go-ole/go-ole v1.3.0 // indirect 45 | github.com/gogo/protobuf v1.3.2 // indirect 46 | github.com/golang/protobuf v1.5.4 // indirect 47 | github.com/google/uuid v1.6.0 // indirect 48 | github.com/hashicorp/go-hclog v1.6.2 // indirect 49 | github.com/hashicorp/go-plugin v1.6.0 // indirect 50 | github.com/hashicorp/hcl v1.0.0 // indirect 51 | github.com/hashicorp/yamux v0.1.1 // indirect 52 | github.com/jackc/pgpassfile v1.0.0 // indirect 53 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 54 | github.com/jackc/puddle/v2 v2.2.2 // indirect 55 | github.com/klauspost/compress v1.17.7 // indirect 56 | github.com/kr/pretty v0.3.1 // indirect 57 | github.com/kr/text v0.2.0 // indirect 58 | github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a // indirect 59 | github.com/magiconair/properties v1.8.7 // indirect 60 | github.com/mattn/go-colorable v0.1.13 // indirect 61 | github.com/mattn/go-isatty v0.0.20 // indirect 62 | github.com/mfridman/interpolate v0.0.2 // indirect 63 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 64 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect 65 | github.com/moby/docker-image-spec v1.3.1 // indirect 66 | github.com/moby/patternmatcher v0.6.0 // indirect 67 | github.com/moby/sys/sequential v0.5.0 // indirect 68 | github.com/moby/sys/user v0.1.0 // indirect 69 | github.com/moby/term v0.5.0 // indirect 70 | github.com/morikuni/aec v1.0.0 // indirect 71 | github.com/oklog/run v1.1.0 // indirect 72 | github.com/opencontainers/go-digest v1.0.0 // indirect 73 | github.com/opencontainers/image-spec v1.1.0 // indirect 74 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 75 | github.com/pkg/errors v0.9.1 // indirect 76 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 77 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 78 | github.com/prometheus/client_model v0.6.0 // indirect 79 | github.com/prometheus/common v0.51.1 // indirect 80 | github.com/prometheus/procfs v0.13.0 // indirect 81 | github.com/rogpeppe/go-internal v1.13.1 // indirect 82 | github.com/sagikazarmark/locafero v0.4.0 // indirect 83 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 84 | github.com/sethvargo/go-retry v0.3.0 // indirect 85 | github.com/shirou/gopsutil/v3 v3.24.2 // indirect 86 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 87 | github.com/sirupsen/logrus v1.9.3 // indirect 88 | github.com/sourcegraph/conc v0.3.0 // indirect 89 | github.com/spf13/afero v1.11.0 // indirect 90 | github.com/spf13/cast v1.6.0 // indirect 91 | github.com/subosito/gotenv v1.6.0 // indirect 92 | github.com/tklauser/go-sysconf v0.3.13 // indirect 93 | github.com/tklauser/numcpus v0.7.0 // indirect 94 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 95 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 96 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 97 | go.opentelemetry.io/otel v1.34.0 // indirect 98 | go.opentelemetry.io/otel/metric v1.34.0 // indirect 99 | go.uber.org/dig v1.17.1 // indirect 100 | go.uber.org/multierr v1.11.0 // indirect 101 | go.uber.org/zap v1.27.0 // indirect 102 | golang.org/x/crypto v0.31.0 // indirect 103 | golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect 104 | golang.org/x/net v0.33.0 // indirect 105 | golang.org/x/sync v0.10.0 // indirect 106 | golang.org/x/sys v0.28.0 // indirect 107 | golang.org/x/text v0.21.0 // indirect 108 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect 109 | google.golang.org/protobuf v1.35.2 // indirect 110 | gopkg.in/ini.v1 v1.67.0 // indirect 111 | gopkg.in/yaml.v3 v3.0.1 // indirect 112 | ) 113 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= 4 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 5 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 6 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 7 | github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= 8 | github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= 9 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 10 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 11 | github.com/Microsoft/hcsshim v0.12.1 h1:ahSMCguNOQMvTV7wWLknLhpieyqA2hUyEb3j6R+6B/c= 12 | github.com/Microsoft/hcsshim v0.12.1/go.mod h1:RZV12pcHCXQ42XnlQ3pz6FZfmrC1C+R4gaOHhRNML1g= 13 | github.com/Shopify/sarama v1.37.2 h1:LoBbU0yJPte0cE5TZCGdlzZRmMgMtZU/XgnUKZg9Cv4= 14 | github.com/Shopify/sarama v1.37.2/go.mod h1:Nxye/E+YPru//Bpaorfhc3JsSGYwCaDDj+R4bK52U5o= 15 | github.com/apache/thrift v0.19.0 h1:sOqkWPzMj7w6XaYbJQG7m4sGqVolaW/0D28Ln7yPzMk= 16 | github.com/apache/thrift v0.19.0/go.mod h1:SUALL216IiaOw2Oy+5Vs9lboJ/t9g40C+G07Dc0QC1I= 17 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 18 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 19 | github.com/bsm/sarama-cluster v2.1.13+incompatible h1:bqU3gMJbWZVxLZ9PGWVKP05yOmFXUlfw61RBwuE3PYU= 20 | github.com/bsm/sarama-cluster v2.1.13+incompatible/go.mod h1:r7ao+4tTNXvWm+VRpRJchr2kQhqxgmAp2iEX5W96gMM= 21 | github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= 22 | github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= 23 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 24 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 25 | github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= 26 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 27 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 28 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 29 | github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= 30 | github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= 31 | github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM= 32 | github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= 33 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 34 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 35 | github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= 36 | github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= 37 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 38 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 39 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 40 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 41 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 42 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 43 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 44 | github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg= 45 | github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw= 46 | github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= 47 | github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= 48 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 49 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 50 | github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= 51 | github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 52 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 53 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 54 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 55 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 56 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 57 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 58 | github.com/eapache/go-resiliency v1.5.0 h1:dRsaR00whmQD+SgVKlq/vCRFNgtEb5yppyeVos3Yce0= 59 | github.com/eapache/go-resiliency v1.5.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= 60 | github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= 61 | github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= 62 | github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= 63 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 64 | github.com/elastic/elastic-transport-go/v8 v8.4.0 h1:EKYiH8CHd33BmMna2Bos1rDNMM89+hdgcymI+KzJCGE= 65 | github.com/elastic/elastic-transport-go/v8 v8.4.0/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk= 66 | github.com/elastic/go-elasticsearch/v8 v8.12.1 h1:QcuFK5LaZS0pSIj/eAEsxmJWmMo7tUs1aVBbzdIgtnE= 67 | github.com/elastic/go-elasticsearch/v8 v8.12.1/go.mod h1:wSzJYrrKPZQ8qPuqAqc6KMR4HrBfHnZORvyL+FMFqq0= 68 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 69 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 70 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 71 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 72 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 73 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 74 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 75 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 76 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 77 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 78 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 79 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 80 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 81 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 82 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 83 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 84 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 85 | github.com/gocql/gocql v1.3.2 h1:ox3T+R7VFibHSIGxRkuUi1uIvAv8jBHCWxc+9aFQ/LA= 86 | github.com/gocql/gocql v1.3.2/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= 87 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 88 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 89 | github.com/golang/glog v1.2.3 h1:oDTdz9f5VGVVNGu/Q7UXKWYsD0873HXLHdJUNBsSEKM= 90 | github.com/golang/glog v1.2.3/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= 91 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 92 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 93 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 94 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 95 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 96 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 97 | github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= 98 | github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 99 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 100 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 101 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 102 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 103 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 104 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 105 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= 106 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= 107 | github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= 108 | github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= 109 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 110 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 111 | github.com/hashicorp/go-hclog v1.6.2 h1:NOtoftovWkDheyUM/8JW3QMiXyxJK3uHRK7wV04nD2I= 112 | github.com/hashicorp/go-hclog v1.6.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 113 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 114 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 115 | github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= 116 | github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= 117 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 118 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 119 | github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 120 | github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 121 | github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 122 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 123 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 124 | github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= 125 | github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= 126 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 127 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 128 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 129 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 130 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 131 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 132 | github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= 133 | github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= 134 | github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 135 | github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 136 | github.com/jaegertracing/jaeger v1.55.0 h1:IJHzKb2B9EYQyKlE7VSoKzNP3emHeqZWnWrKj+kYzzs= 137 | github.com/jaegertracing/jaeger v1.55.0/go.mod h1:S884Mz8H+iGI8Ealq6sM9QzSOeU6P+nbFkYw7uww8CI= 138 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 139 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 140 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 141 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 142 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 143 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 144 | github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= 145 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 146 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 147 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 148 | github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= 149 | github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= 150 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 151 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 152 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 153 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 154 | github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= 155 | github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 156 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 157 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 158 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 159 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 160 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 161 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 162 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 163 | github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a h1:3Bm7EwfUQUvhNeKIkUct/gl9eod1TcXuj8stxvi/GoI= 164 | github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= 165 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 166 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 167 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 168 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 169 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 170 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 171 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 172 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 173 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 174 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 175 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 176 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 177 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 178 | github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= 179 | github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= 180 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= 181 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 182 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= 183 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 184 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 185 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 186 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 187 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 188 | github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= 189 | github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= 190 | github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= 191 | github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= 192 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 193 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 194 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 195 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 196 | github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 197 | github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 198 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= 199 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= 200 | github.com/olivere/elastic v6.2.37+incompatible h1:UfSGJem5czY+x/LqxgeCBgjDn6St+z8OnsCuxwD3L0U= 201 | github.com/olivere/elastic v6.2.37+incompatible/go.mod h1:J+q1zQJTgAz9woqsbVRqGeB5G1iqDKVBWLNSYW8yfJ8= 202 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 203 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 204 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 205 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 206 | github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= 207 | github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 208 | github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= 209 | github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= 210 | github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 211 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 212 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 213 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 214 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 215 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 216 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 217 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 218 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= 219 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 220 | github.com/pressly/goose/v3 v3.24.1 h1:bZmxRco2uy5uu5Ng1MMVEfYsFlrMJI+e/VMXHQ3C4LY= 221 | github.com/pressly/goose/v3 v3.24.1/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528DV+4DORug= 222 | github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= 223 | github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= 224 | github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= 225 | github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= 226 | github.com/prometheus/common v0.51.1 h1:eIjN50Bwglz6a/c3hAgSMcofL3nD+nFQkV6Dd4DsQCw= 227 | github.com/prometheus/common v0.51.1/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q= 228 | github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= 229 | github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= 230 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= 231 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 232 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 233 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 234 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 235 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 236 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 237 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 238 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 239 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 240 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 241 | github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= 242 | github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= 243 | github.com/shirou/gopsutil/v3 v3.24.2 h1:kcR0erMbLg5/3LcInpw0X/rrPSqq4CDPyI6A6ZRC18Y= 244 | github.com/shirou/gopsutil/v3 v3.24.2/go.mod h1:tSg/594BcA+8UdQU2XcW803GWYgdtauFFPgJCJKZlVk= 245 | github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= 246 | github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= 247 | github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= 248 | github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= 249 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 250 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 251 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 252 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 253 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 254 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 255 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 256 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 257 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 258 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 259 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 260 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 261 | github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= 262 | github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= 263 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 264 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 265 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 266 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 267 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 268 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 269 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 270 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 271 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 272 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 273 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 274 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 275 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 276 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 277 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 278 | github.com/testcontainers/testcontainers-go v0.32.0 h1:ug1aK08L3gCHdhknlTTwWjPHPS+/alvLJU/DRxTD/ME= 279 | github.com/testcontainers/testcontainers-go v0.32.0/go.mod h1:CRHrzHLQhlXUsa5gXjTOfqIEJcrK5+xMDmBr/WMI88E= 280 | github.com/testcontainers/testcontainers-go/modules/postgres v0.32.0 h1:ZE4dTdswj3P0j71nL+pL0m2e5HTXJwPoIFr+DDgdPaU= 281 | github.com/testcontainers/testcontainers-go/modules/postgres v0.32.0/go.mod h1:njrNuyuoF2fjhVk6TG/R3Oeu82YwfYkbf5WVTyBXhV4= 282 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 283 | github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= 284 | github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= 285 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 286 | github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= 287 | github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY= 288 | github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= 289 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 290 | github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= 291 | github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= 292 | github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= 293 | github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= 294 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 295 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 296 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 297 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 298 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 299 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 300 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 301 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 302 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= 303 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= 304 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= 305 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= 306 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 307 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 308 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= 309 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= 310 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= 311 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= 312 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 313 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 314 | go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= 315 | go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= 316 | go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= 317 | go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= 318 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 319 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 320 | go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= 321 | go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= 322 | go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= 323 | go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= 324 | go.uber.org/fx v1.21.0 h1:qqD6k7PyFHONffW5speYx403ywanuASqU4Rqdpc22XY= 325 | go.uber.org/fx v1.21.0/go.mod h1:HT2M7d7RHo+ebKGh9NRcrsrHHfpZ60nW3QRubMRfv48= 326 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 327 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 328 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 329 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 330 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 331 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 332 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 333 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 334 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 335 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 336 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 337 | golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= 338 | golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= 339 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 340 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 341 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 342 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 343 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 344 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 345 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 346 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 347 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 348 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 349 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 350 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 351 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 352 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 353 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 354 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 355 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 356 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 357 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 358 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 359 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 360 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 361 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 362 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 363 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 364 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 365 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 366 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 367 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 368 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 369 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 370 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 371 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 372 | golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= 373 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 374 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 375 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 376 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 377 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 378 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 379 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 380 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 381 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 382 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 383 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 384 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 385 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 386 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 387 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 388 | google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= 389 | google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a h1:OAiGFfOiA0v9MRYsSidp3ubZaBnteRUyn3xB2ZQ5G/E= 390 | google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY= 391 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o= 392 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= 393 | google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= 394 | google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= 395 | google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= 396 | google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 397 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 398 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 399 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 400 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 401 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 402 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 403 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 404 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 405 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 406 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 407 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 408 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 409 | modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= 410 | modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= 411 | modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= 412 | modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= 413 | modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= 414 | modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= 415 | modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= 416 | modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= 417 | modernc.org/sqlite v1.34.1 h1:u3Yi6M0N8t9yKRDwhXcyp1eS5/ErhPTBggxWFuR6Hfk= 418 | modernc.org/sqlite v1.34.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= 419 | modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= 420 | modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= 421 | modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 422 | modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 423 | -------------------------------------------------------------------------------- /hack/.gitignore: -------------------------------------------------------------------------------- 1 | jaeger 2 | charts -------------------------------------------------------------------------------- /hack/config.yaml: -------------------------------------------------------------------------------- 1 | { 2 | "database_url":"postgresql://postgres:password@localhost:5432/jaeger" 3 | } -------------------------------------------------------------------------------- /hack/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/env sh 2 | 3 | cp /app/jaeger-postgresql /mnt/plugin 4 | chmod +x /mnt/plugin/jaeger-postgresql 5 | -------------------------------------------------------------------------------- /internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | ) 8 | 9 | // New returns a new logger. 10 | func New(loglevelStr *string) (*slog.Logger, error) { 11 | levelFn := func() (slog.Level, error) { 12 | if loglevelStr == nil { 13 | return slog.LevelWarn, nil 14 | } 15 | 16 | switch *loglevelStr { 17 | case "info": 18 | return slog.LevelInfo, nil 19 | case "warn": 20 | return slog.LevelWarn, nil 21 | case "error": 22 | return slog.LevelError, nil 23 | case "debug": 24 | return slog.LevelDebug, nil 25 | default: 26 | return 0, fmt.Errorf("invalid log level: %s", *loglevelStr) 27 | } 28 | } 29 | level, err := levelFn() 30 | if err != nil { 31 | return nil, fmt.Errorf("failed to build logger: %w", err) 32 | } 33 | 34 | return slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: level})), nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/sql/copyfrom.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.25.0 4 | // source: copyfrom.go 5 | 6 | package sql 7 | -------------------------------------------------------------------------------- /internal/sql/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.25.0 4 | 5 | package sql 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/jackc/pgx/v5" 11 | "github.com/jackc/pgx/v5/pgconn" 12 | ) 13 | 14 | type DBTX interface { 15 | Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) 16 | Query(context.Context, string, ...interface{}) (pgx.Rows, error) 17 | QueryRow(context.Context, string, ...interface{}) pgx.Row 18 | } 19 | 20 | func New(db DBTX) *Queries { 21 | return &Queries{db: db} 22 | } 23 | 24 | type Queries struct { 25 | db DBTX 26 | } 27 | 28 | func (q *Queries) WithTx(tx pgx.Tx) *Queries { 29 | return &Queries{ 30 | db: tx, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/sql/migrations.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "log/slog" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/pressly/goose/v3" 11 | 12 | _ "github.com/jackc/pgx/v5/stdlib" 13 | ) 14 | 15 | var ( 16 | mu sync.Mutex 17 | 18 | //go:embed migrations/*.sql 19 | migrations embed.FS 20 | ) 21 | 22 | var _ goose.Logger = (*gooseLogger)(nil) 23 | 24 | type gooseLogger struct { 25 | *slog.Logger 26 | } 27 | 28 | func (l *gooseLogger) Fatal(v ...interface{}) { 29 | l.Logger.Error(fmt.Sprint(v...)) 30 | } 31 | 32 | func (l *gooseLogger) Fatalf(msg string, v ...interface{}) { 33 | l.Logger.Error(fmt.Sprintf(msg, v...)) 34 | } 35 | 36 | func (l *gooseLogger) Print(v ...interface{}) { 37 | l.Logger.Info(fmt.Sprint(v...)) 38 | } 39 | 40 | func (l *gooseLogger) Println(v ...interface{}) { 41 | l.Logger.Info(fmt.Sprint(v...)) 42 | } 43 | 44 | func (l *gooseLogger) Printf(msg string, v ...interface{}) { 45 | trimmed := strings.Trim(msg, "\n") 46 | l.Logger.Info(fmt.Sprintf(trimmed, v...)) 47 | } 48 | 49 | func Migrate(logger *slog.Logger, connStr string) error { 50 | mu.Lock() 51 | defer mu.Unlock() 52 | 53 | goose.SetLogger(&gooseLogger{logger}) 54 | 55 | goose.SetBaseFS(migrations) 56 | 57 | goose.SetDialect("pgx") 58 | 59 | db, err := goose.OpenDBWithDriver("pgx", connStr) 60 | if err != nil { 61 | return fmt.Errorf("connecting to db for migrations: %w", err) 62 | } 63 | defer db.Close() 64 | 65 | if err := goose.SetDialect("postgres"); err != nil { 66 | return fmt.Errorf("setting postgres dialect for migrations: %w", err) 67 | } 68 | 69 | if err := goose.Up(db, "migrations"); err != nil { 70 | return fmt.Errorf("unable to migrate database: %w", err) 71 | } 72 | 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /internal/sql/migrations/001_initial.sql: -------------------------------------------------------------------------------- 1 | 2 | -- +goose Up 3 | 4 | CREATE TYPE SPANKIND AS ENUM ('server', 'client', 'unspecified', 'producer', 'consumer', 'ephemeral', 'internal'); 5 | 6 | CREATE TABLE services ( 7 | id BIGSERIAL PRIMARY KEY, 8 | name TEXT NOT NULL UNIQUE 9 | ); 10 | 11 | CREATE TABLE operations ( 12 | id BIGSERIAL PRIMARY KEY, 13 | name TEXT NOT NULL, 14 | service_id BIGINT REFERENCES services(id) NOT NULL, 15 | kind SPANKIND NOT NULL, 16 | 17 | UNIQUE (name, kind, service_id) 18 | ); 19 | 20 | -- here our primary key is not named id as is customary. Instead we have named 21 | -- it hack_id. This is to help us differentiate it from span_id. We can't use 22 | -- span_id as our primary key because its not guaranteed to be unique as a 23 | -- compatability measure with zipkin. 24 | CREATE TABLE spans ( 25 | hack_id BIGSERIAL PRIMARY KEY, 26 | span_id BYTEA NOT NULL, 27 | trace_id BYTEA NOT NULL, 28 | operation_id BIGINT REFERENCES operations(id) NOT NULL, 29 | flags BIGINT NOT NULL, 30 | start_time TIMESTAMP NOT NULL, 31 | duration INTERVAL NOT NULL, 32 | tags JSONB, 33 | service_id BIGINT REFERENCES services(id) NOT NULL, 34 | process_id TEXT NOT NULL, 35 | process_tags JSONB NOT NULL, 36 | warnings TEXT[], 37 | logs JSONB, 38 | kind SPANKIND NOT NULL, 39 | refs JSONB NOT NULL 40 | ); 41 | 42 | -- +goose Down 43 | 44 | DROP TABLE spans; 45 | DROP TABLE operations; 46 | DROP TABLE services; 47 | 48 | DROP TYPE SPANKIND; -------------------------------------------------------------------------------- /internal/sql/migrations/002_indexes.sql: -------------------------------------------------------------------------------- 1 | 2 | -- +goose Up 3 | 4 | CREATE INDEX IF NOT EXISTS idx_trace_id ON spans (trace_id); 5 | CREATE INDEX IF NOT EXISTS idx_services_name ON services(name); 6 | CREATE INDEX IF NOT EXISTS idx_operations_name ON operations(name); 7 | CREATE INDEX IF NOT EXISTS idx_spans_operation_service ON spans(operation_id, service_id); 8 | CREATE INDEX IF NOT EXISTS idx_spans_start_duration ON spans(start_time, duration); 9 | CREATE INDEX IF NOT EXISTS idx_spans_start_time ON spans(start_time); 10 | CREATE INDEX IF NOT EXISTS idx_spans_duration ON spans(duration); 11 | 12 | -- +goose Down 13 | 14 | DROP INDEX idx_trace_id ON spans (trace_id); 15 | DROP INDEX idx_services_name ON services(name); 16 | DROP INDEX idx_operations_name ON operations(name); 17 | DROP INDEX idx_spans_operation_service ON spans(operation_id, service_id); 18 | DROP INDEX idx_spans_start_duration ON spans(start_time, duration); 19 | DROP INDEX idx_spans_start_time ON spans(start_time); 20 | DROP INDEX idx_spans_duration ON spans(duration); 21 | -------------------------------------------------------------------------------- /internal/sql/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.25.0 4 | 5 | package sql 6 | 7 | import ( 8 | "database/sql/driver" 9 | "fmt" 10 | 11 | "github.com/jackc/pgx/v5/pgtype" 12 | ) 13 | 14 | type Spankind string 15 | 16 | const ( 17 | SpankindServer Spankind = "server" 18 | SpankindClient Spankind = "client" 19 | SpankindUnspecified Spankind = "unspecified" 20 | SpankindProducer Spankind = "producer" 21 | SpankindConsumer Spankind = "consumer" 22 | SpankindEphemeral Spankind = "ephemeral" 23 | SpankindInternal Spankind = "internal" 24 | ) 25 | 26 | func (e *Spankind) Scan(src interface{}) error { 27 | switch s := src.(type) { 28 | case []byte: 29 | *e = Spankind(s) 30 | case string: 31 | *e = Spankind(s) 32 | default: 33 | return fmt.Errorf("unsupported scan type for Spankind: %T", src) 34 | } 35 | return nil 36 | } 37 | 38 | type NullSpankind struct { 39 | Spankind Spankind 40 | Valid bool // Valid is true if Spankind is not NULL 41 | } 42 | 43 | // Scan implements the Scanner interface. 44 | func (ns *NullSpankind) Scan(value interface{}) error { 45 | if value == nil { 46 | ns.Spankind, ns.Valid = "", false 47 | return nil 48 | } 49 | ns.Valid = true 50 | return ns.Spankind.Scan(value) 51 | } 52 | 53 | // Value implements the driver Valuer interface. 54 | func (ns NullSpankind) Value() (driver.Value, error) { 55 | if !ns.Valid { 56 | return nil, nil 57 | } 58 | return string(ns.Spankind), nil 59 | } 60 | 61 | type Operation struct { 62 | ID int64 63 | Name string 64 | ServiceID int64 65 | Kind Spankind 66 | } 67 | 68 | type Service struct { 69 | ID int64 70 | Name string 71 | } 72 | 73 | type Span struct { 74 | HackID int64 75 | SpanID []byte 76 | TraceID []byte 77 | OperationID int64 78 | Flags int64 79 | StartTime pgtype.Timestamp 80 | Duration pgtype.Interval 81 | Tags []byte 82 | ServiceID int64 83 | ProcessID string 84 | ProcessTags []byte 85 | Warnings []string 86 | Logs []byte 87 | Kind Spankind 88 | Refs []byte 89 | } 90 | -------------------------------------------------------------------------------- /internal/sql/query.sql: -------------------------------------------------------------------------------- 1 | -- name: GetOperations :many 2 | SELECT operations.name, operations.kind 3 | FROM operations 4 | INNER JOIN services ON (operations.service_id = services.id) 5 | WHERE services.name = sqlc.arg(service_name)::VARCHAR 6 | ORDER BY operations.name ASC; 7 | 8 | -- name: GetServices :many 9 | SELECT services.name 10 | FROM services 11 | ORDER BY services.name ASC; 12 | 13 | -- -- name: GetDependencies :many 14 | -- SELECT 15 | -- COUNT(*) AS call_count, 16 | -- source_services.name as parent, 17 | -- child_services.name as child, 18 | -- '' as source 19 | -- FROM spanrefs 20 | -- INNER JOIN spans AS source_spans ON (source_spans.span_id = spanrefs.source_span_id) 21 | -- INNER JOIN spans AS child_spans ON (child_spans.span_id = spanrefs.child_span_id) 22 | -- INNER JOIN services AS source_services ON (source_spans.service_id = source_services.id) 23 | -- INNER JOIN services AS child_services ON (child_spans.service_id = child_services.id) 24 | -- GROUP BY source_services.name, child_services.name; 25 | 26 | -- -- name: FindTraceIDs :many 27 | -- SELECT DISTINCT spans.trace_id 28 | -- FROM spans 29 | -- INNER JOIN operations ON (operations.id = spans.operation_id) 30 | -- INNER JOIN services ON (services.id = spans.service_id) 31 | -- WHERE 32 | -- (services.name = sqlc.arg(service_name)::VARCHAR OR sqlc.arg(service_name_enable)::BOOLEAN = FALSE) AND 33 | -- (operations.name = sqlc.arg(operation_name)::VARCHAR OR sqlc.arg(operation_name_enable)::BOOLEAN = FALSE) AND 34 | -- (start_time >= sqlc.arg(start_time_minimum)::TIMESTAMPTZ OR sqlc.arg(start_time_minimum_enable)::BOOLEAN = FALSE) AND 35 | -- (start_time < sqlc.arg(start_time_maximum)::TIMESTAMPTZ OR sqlc.arg(start_time_maximum_enable)::BOOLEAN = FALSE) AND 36 | -- (duration > sqlc.arg(duration_minimum)::INTERVAL OR sqlc.arg(duration_minimum_enable)::BOOLEAN = FALSE) AND 37 | -- (duration < sqlc.arg(duration_maximum)::INTERVAL OR sqlc.arg(duration_maximum_enable)::BOOLEAN = FALSE) 38 | -- ; 39 | --LIMIT sqlc.arg(limit)::INT; 40 | 41 | -- name: UpsertService :exec 42 | INSERT INTO services (name) 43 | VALUES (sqlc.arg(name)::VARCHAR) ON CONFLICT(name) DO NOTHING RETURNING id; 44 | 45 | -- name: GetServiceID :one 46 | SELECT id 47 | FROM services 48 | WHERE name = sqlc.arg(name)::TEXT; 49 | 50 | -- name: UpsertOperation :exec 51 | INSERT INTO operations (name, service_id, kind) 52 | VALUES ( 53 | sqlc.arg(name)::TEXT, 54 | sqlc.arg(service_id)::BIGINT, 55 | sqlc.arg(kind)::SPANKIND 56 | ) ON CONFLICT(name, service_id, kind) DO NOTHING RETURNING id; 57 | 58 | -- name: GetOperationID :one 59 | SELECT id 60 | FROM operations 61 | WHERE 62 | name = sqlc.arg(name)::TEXT AND 63 | service_id = sqlc.arg(service_id)::BIGINT AND 64 | kind = sqlc.arg(kind)::SPANKIND; 65 | 66 | -- name: GetTraceSpans :many 67 | SELECT 68 | spans.span_id as span_id, 69 | spans.trace_id as trace_id, 70 | operations.name as operation_name, 71 | spans.flags as flags, 72 | spans.start_time as start_time, 73 | spans.duration as duration, 74 | spans.tags as tags, 75 | spans.process_id as process_id, 76 | spans.warnings as warnings, 77 | spans.kind as kind, 78 | services.name as process_name, 79 | spans.process_tags as process_tags, 80 | spans.logs as logs, 81 | spans.refs as refs 82 | FROM spans 83 | INNER JOIN operations ON (spans.operation_id = operations.id) 84 | INNER JOIN services ON (spans.service_id = services.id) 85 | WHERE trace_id = sqlc.arg(trace_id)::BYTEA; 86 | 87 | -- name: InsertSpan :one 88 | INSERT INTO spans ( 89 | span_id, 90 | trace_id, 91 | operation_id, 92 | flags, 93 | start_time, 94 | duration, 95 | tags, 96 | service_id, 97 | process_id, 98 | process_tags, 99 | warnings, 100 | kind, 101 | logs, 102 | refs 103 | ) 104 | VALUES( 105 | sqlc.arg(span_id)::BYTEA, 106 | sqlc.arg(trace_id)::BYTEA, 107 | sqlc.arg(operation_id)::BIGINT, 108 | sqlc.arg(flags)::BIGINT, 109 | sqlc.arg(start_time)::TIMESTAMP, 110 | sqlc.arg(duration)::INTERVAL, 111 | sqlc.arg(tags)::JSONB, 112 | sqlc.arg(service_id)::BIGINT, 113 | sqlc.arg(process_id)::TEXT, 114 | sqlc.arg(process_tags)::JSONB, 115 | sqlc.arg(warnings)::TEXT[], 116 | sqlc.arg(kind)::SPANKIND, 117 | sqlc.arg(logs)::JSONB, 118 | sqlc.arg(refs)::JSONB 119 | ) 120 | RETURNING spans.hack_id; 121 | 122 | -- name: CleanSpans :execrows 123 | 124 | DELETE FROM spans 125 | WHERE spans.start_time < sqlc.arg(prune_before)::TIMESTAMP; 126 | 127 | -- name: GetSpansDiskSize :one 128 | 129 | SELECT pg_total_relation_size('spans'); 130 | 131 | -- name: GetSpansCount :one 132 | 133 | SELECT COUNT(*) FROM spans; 134 | 135 | -- name: FindTraceIDs :many 136 | 137 | SELECT DISTINCT spans.trace_id as trace_id 138 | FROM spans 139 | INNER JOIN operations ON (operations.id = spans.operation_id) 140 | INNER JOIN services ON (services.id = spans.service_id) 141 | WHERE 142 | (services.name = sqlc.arg(service_name)::VARCHAR OR sqlc.arg(service_name_enable_filter)::BOOLEAN = FALSE) AND 143 | (operations.name = sqlc.arg(operation_name)::VARCHAR OR sqlc.arg(operation_name_enable_filter)::BOOLEAN = FALSE) AND 144 | (start_time >= sqlc.arg(start_time_minimum)::TIMESTAMP OR sqlc.arg(start_time_minimum_enable_filter)::BOOLEAN = FALSE) AND 145 | (start_time <= sqlc.arg(start_time_maximum)::TIMESTAMP OR sqlc.arg(start_time_maximum_enable_filter)::BOOLEAN = FALSE) AND 146 | (duration >= sqlc.arg(duration_minimum)::INTERVAL OR sqlc.arg(duration_minimum_enable_filter)::BOOLEAN = FALSE) AND 147 | (duration <= sqlc.arg(duration_maximum)::INTERVAL OR sqlc.arg(duration_maximum_enable_filter)::BOOLEAN = FALSE) 148 | LIMIT sqlc.arg(num_traces); -------------------------------------------------------------------------------- /internal/sql/query.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.25.0 4 | // source: query.sql 5 | 6 | package sql 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/jackc/pgx/v5/pgtype" 12 | ) 13 | 14 | const cleanSpans = `-- name: CleanSpans :execrows 15 | 16 | DELETE FROM spans 17 | WHERE spans.start_time < $1::TIMESTAMP 18 | ` 19 | 20 | func (q *Queries) CleanSpans(ctx context.Context, pruneBefore pgtype.Timestamp) (int64, error) { 21 | result, err := q.db.Exec(ctx, cleanSpans, pruneBefore) 22 | if err != nil { 23 | return 0, err 24 | } 25 | return result.RowsAffected(), nil 26 | } 27 | 28 | const findTraceIDs = `-- name: FindTraceIDs :many 29 | 30 | SELECT DISTINCT spans.trace_id as trace_id 31 | FROM spans 32 | INNER JOIN operations ON (operations.id = spans.operation_id) 33 | INNER JOIN services ON (services.id = spans.service_id) 34 | WHERE 35 | (services.name = $1::VARCHAR OR $2::BOOLEAN = FALSE) AND 36 | (operations.name = $3::VARCHAR OR $4::BOOLEAN = FALSE) AND 37 | (start_time >= $5::TIMESTAMP OR $6::BOOLEAN = FALSE) AND 38 | (start_time <= $7::TIMESTAMP OR $8::BOOLEAN = FALSE) AND 39 | (duration >= $9::INTERVAL OR $10::BOOLEAN = FALSE) AND 40 | (duration <= $11::INTERVAL OR $12::BOOLEAN = FALSE) 41 | LIMIT $13 42 | ` 43 | 44 | type FindTraceIDsParams struct { 45 | ServiceName string 46 | ServiceNameEnableFilter bool 47 | OperationName string 48 | OperationNameEnableFilter bool 49 | StartTimeMinimum pgtype.Timestamp 50 | StartTimeMinimumEnableFilter bool 51 | StartTimeMaximum pgtype.Timestamp 52 | StartTimeMaximumEnableFilter bool 53 | DurationMinimum pgtype.Interval 54 | DurationMinimumEnableFilter bool 55 | DurationMaximum pgtype.Interval 56 | DurationMaximumEnableFilter bool 57 | NumTraces int32 58 | } 59 | 60 | func (q *Queries) FindTraceIDs(ctx context.Context, arg FindTraceIDsParams) ([][]byte, error) { 61 | rows, err := q.db.Query(ctx, findTraceIDs, 62 | arg.ServiceName, 63 | arg.ServiceNameEnableFilter, 64 | arg.OperationName, 65 | arg.OperationNameEnableFilter, 66 | arg.StartTimeMinimum, 67 | arg.StartTimeMinimumEnableFilter, 68 | arg.StartTimeMaximum, 69 | arg.StartTimeMaximumEnableFilter, 70 | arg.DurationMinimum, 71 | arg.DurationMinimumEnableFilter, 72 | arg.DurationMaximum, 73 | arg.DurationMaximumEnableFilter, 74 | arg.NumTraces, 75 | ) 76 | if err != nil { 77 | return nil, err 78 | } 79 | defer rows.Close() 80 | var items [][]byte 81 | for rows.Next() { 82 | var trace_id []byte 83 | if err := rows.Scan(&trace_id); err != nil { 84 | return nil, err 85 | } 86 | items = append(items, trace_id) 87 | } 88 | if err := rows.Err(); err != nil { 89 | return nil, err 90 | } 91 | return items, nil 92 | } 93 | 94 | const getOperationID = `-- name: GetOperationID :one 95 | SELECT id 96 | FROM operations 97 | WHERE 98 | name = $1::TEXT AND 99 | service_id = $2::BIGINT AND 100 | kind = $3::SPANKIND 101 | ` 102 | 103 | type GetOperationIDParams struct { 104 | Name string 105 | ServiceID int64 106 | Kind Spankind 107 | } 108 | 109 | func (q *Queries) GetOperationID(ctx context.Context, arg GetOperationIDParams) (int64, error) { 110 | row := q.db.QueryRow(ctx, getOperationID, arg.Name, arg.ServiceID, arg.Kind) 111 | var id int64 112 | err := row.Scan(&id) 113 | return id, err 114 | } 115 | 116 | const getOperations = `-- name: GetOperations :many 117 | SELECT operations.name, operations.kind 118 | FROM operations 119 | INNER JOIN services ON (operations.service_id = services.id) 120 | WHERE services.name = $1::VARCHAR 121 | ORDER BY operations.name ASC 122 | ` 123 | 124 | type GetOperationsRow struct { 125 | Name string 126 | Kind Spankind 127 | } 128 | 129 | func (q *Queries) GetOperations(ctx context.Context, serviceName string) ([]GetOperationsRow, error) { 130 | rows, err := q.db.Query(ctx, getOperations, serviceName) 131 | if err != nil { 132 | return nil, err 133 | } 134 | defer rows.Close() 135 | var items []GetOperationsRow 136 | for rows.Next() { 137 | var i GetOperationsRow 138 | if err := rows.Scan(&i.Name, &i.Kind); err != nil { 139 | return nil, err 140 | } 141 | items = append(items, i) 142 | } 143 | if err := rows.Err(); err != nil { 144 | return nil, err 145 | } 146 | return items, nil 147 | } 148 | 149 | const getServiceID = `-- name: GetServiceID :one 150 | SELECT id 151 | FROM services 152 | WHERE name = $1::TEXT 153 | ` 154 | 155 | func (q *Queries) GetServiceID(ctx context.Context, name string) (int64, error) { 156 | row := q.db.QueryRow(ctx, getServiceID, name) 157 | var id int64 158 | err := row.Scan(&id) 159 | return id, err 160 | } 161 | 162 | const getServices = `-- name: GetServices :many 163 | SELECT services.name 164 | FROM services 165 | ORDER BY services.name ASC 166 | ` 167 | 168 | func (q *Queries) GetServices(ctx context.Context) ([]string, error) { 169 | rows, err := q.db.Query(ctx, getServices) 170 | if err != nil { 171 | return nil, err 172 | } 173 | defer rows.Close() 174 | var items []string 175 | for rows.Next() { 176 | var name string 177 | if err := rows.Scan(&name); err != nil { 178 | return nil, err 179 | } 180 | items = append(items, name) 181 | } 182 | if err := rows.Err(); err != nil { 183 | return nil, err 184 | } 185 | return items, nil 186 | } 187 | 188 | const getSpansCount = `-- name: GetSpansCount :one 189 | 190 | WITH span_tables AS ( 191 | SELECT inhrelid::regclass::text AS relname 192 | FROM pg_catalog.pg_class 193 | JOIN pg_catalog.pg_inherits ON inhparent = oid 194 | WHERE relname = 'spans' 195 | UNION 196 | SELECT 'spans' 197 | ) 198 | SELECT sum(n_live_tup) 199 | FROM pg_stat_user_tables 200 | JOIN span_tables USING (relname) 201 | ` 202 | 203 | func (q *Queries) GetSpansCount(ctx context.Context) (int64, error) { 204 | row := q.db.QueryRow(ctx, getSpansCount) 205 | var count int64 206 | err := row.Scan(&count) 207 | return count, err 208 | } 209 | 210 | const getSpansDiskSize = `-- name: GetSpansDiskSize :one 211 | 212 | WITH span_tables AS ( 213 | SELECT inhrelid::regclass::text AS relname 214 | FROM pg_catalog.pg_class 215 | JOIN pg_catalog.pg_inherits ON inhparent = oid 216 | WHERE relname = 'spans' 217 | UNION 218 | SELECT 'spans' 219 | ) 220 | SELECT sum(pg_total_relation_size(relname)) 221 | FROM span_tables 222 | ` 223 | 224 | func (q *Queries) GetSpansDiskSize(ctx context.Context) (int64, error) { 225 | row := q.db.QueryRow(ctx, getSpansDiskSize) 226 | var pg_total_relation_size int64 227 | err := row.Scan(&pg_total_relation_size) 228 | return pg_total_relation_size, err 229 | } 230 | 231 | const getTraceSpans = `-- name: GetTraceSpans :many 232 | SELECT 233 | spans.span_id as span_id, 234 | spans.trace_id as trace_id, 235 | operations.name as operation_name, 236 | spans.flags as flags, 237 | spans.start_time as start_time, 238 | spans.duration as duration, 239 | spans.tags as tags, 240 | spans.process_id as process_id, 241 | spans.warnings as warnings, 242 | spans.kind as kind, 243 | services.name as process_name, 244 | spans.process_tags as process_tags, 245 | spans.logs as logs, 246 | spans.refs as refs 247 | FROM spans 248 | INNER JOIN operations ON (spans.operation_id = operations.id) 249 | INNER JOIN services ON (spans.service_id = services.id) 250 | WHERE trace_id = $1::BYTEA 251 | ` 252 | 253 | type GetTraceSpansRow struct { 254 | SpanID []byte 255 | TraceID []byte 256 | OperationName string 257 | Flags int64 258 | StartTime pgtype.Timestamp 259 | Duration pgtype.Interval 260 | Tags []byte 261 | ProcessID string 262 | Warnings []string 263 | Kind Spankind 264 | ProcessName string 265 | ProcessTags []byte 266 | Logs []byte 267 | Refs []byte 268 | } 269 | 270 | func (q *Queries) GetTraceSpans(ctx context.Context, traceID []byte) ([]GetTraceSpansRow, error) { 271 | rows, err := q.db.Query(ctx, getTraceSpans, traceID) 272 | if err != nil { 273 | return nil, err 274 | } 275 | defer rows.Close() 276 | var items []GetTraceSpansRow 277 | for rows.Next() { 278 | var i GetTraceSpansRow 279 | if err := rows.Scan( 280 | &i.SpanID, 281 | &i.TraceID, 282 | &i.OperationName, 283 | &i.Flags, 284 | &i.StartTime, 285 | &i.Duration, 286 | &i.Tags, 287 | &i.ProcessID, 288 | &i.Warnings, 289 | &i.Kind, 290 | &i.ProcessName, 291 | &i.ProcessTags, 292 | &i.Logs, 293 | &i.Refs, 294 | ); err != nil { 295 | return nil, err 296 | } 297 | items = append(items, i) 298 | } 299 | if err := rows.Err(); err != nil { 300 | return nil, err 301 | } 302 | return items, nil 303 | } 304 | 305 | const insertSpan = `-- name: InsertSpan :one 306 | INSERT INTO spans ( 307 | span_id, 308 | trace_id, 309 | operation_id, 310 | flags, 311 | start_time, 312 | duration, 313 | tags, 314 | service_id, 315 | process_id, 316 | process_tags, 317 | warnings, 318 | kind, 319 | logs, 320 | refs 321 | ) 322 | VALUES( 323 | $1::BYTEA, 324 | $2::BYTEA, 325 | $3::BIGINT, 326 | $4::BIGINT, 327 | $5::TIMESTAMP, 328 | $6::INTERVAL, 329 | $7::JSONB, 330 | $8::BIGINT, 331 | $9::TEXT, 332 | $10::JSONB, 333 | $11::TEXT[], 334 | $12::SPANKIND, 335 | $13::JSONB, 336 | $14::JSONB 337 | ) 338 | RETURNING spans.hack_id 339 | ` 340 | 341 | type InsertSpanParams struct { 342 | SpanID []byte 343 | TraceID []byte 344 | OperationID int64 345 | Flags int64 346 | StartTime pgtype.Timestamp 347 | Duration pgtype.Interval 348 | Tags []byte 349 | ServiceID int64 350 | ProcessID string 351 | ProcessTags []byte 352 | Warnings []string 353 | Kind Spankind 354 | Logs []byte 355 | Refs []byte 356 | } 357 | 358 | func (q *Queries) InsertSpan(ctx context.Context, arg InsertSpanParams) (int64, error) { 359 | row := q.db.QueryRow(ctx, insertSpan, 360 | arg.SpanID, 361 | arg.TraceID, 362 | arg.OperationID, 363 | arg.Flags, 364 | arg.StartTime, 365 | arg.Duration, 366 | arg.Tags, 367 | arg.ServiceID, 368 | arg.ProcessID, 369 | arg.ProcessTags, 370 | arg.Warnings, 371 | arg.Kind, 372 | arg.Logs, 373 | arg.Refs, 374 | ) 375 | var hack_id int64 376 | err := row.Scan(&hack_id) 377 | return hack_id, err 378 | } 379 | 380 | const upsertOperation = `-- name: UpsertOperation :exec 381 | INSERT INTO operations (name, service_id, kind) 382 | VALUES ( 383 | $1::TEXT, 384 | $2::BIGINT, 385 | $3::SPANKIND 386 | ) ON CONFLICT(name, service_id, kind) DO NOTHING RETURNING id 387 | ` 388 | 389 | type UpsertOperationParams struct { 390 | Name string 391 | ServiceID int64 392 | Kind Spankind 393 | } 394 | 395 | func (q *Queries) UpsertOperation(ctx context.Context, arg UpsertOperationParams) error { 396 | _, err := q.db.Exec(ctx, upsertOperation, arg.Name, arg.ServiceID, arg.Kind) 397 | return err 398 | } 399 | 400 | const upsertService = `-- name: UpsertService :exec 401 | 402 | 403 | INSERT INTO services (name) 404 | VALUES ($1::VARCHAR) ON CONFLICT(name) DO NOTHING RETURNING id 405 | ` 406 | 407 | // -- name: GetDependencies :many 408 | // SELECT 409 | // 410 | // COUNT(*) AS call_count, 411 | // source_services.name as parent, 412 | // child_services.name as child, 413 | // '' as source 414 | // 415 | // FROM spanrefs 416 | // 417 | // INNER JOIN spans AS source_spans ON (source_spans.span_id = spanrefs.source_span_id) 418 | // INNER JOIN spans AS child_spans ON (child_spans.span_id = spanrefs.child_span_id) 419 | // INNER JOIN services AS source_services ON (source_spans.service_id = source_services.id) 420 | // INNER JOIN services AS child_services ON (child_spans.service_id = child_services.id) 421 | // 422 | // GROUP BY source_services.name, child_services.name; 423 | // -- name: FindTraceIDs :many 424 | // SELECT DISTINCT spans.trace_id 425 | // FROM spans 426 | // 427 | // INNER JOIN operations ON (operations.id = spans.operation_id) 428 | // INNER JOIN services ON (services.id = spans.service_id) 429 | // 430 | // WHERE 431 | // 432 | // (services.name = sqlc.arg(service_name)::VARCHAR OR sqlc.arg(service_name_enable)::BOOLEAN = FALSE) AND 433 | // (operations.name = sqlc.arg(operation_name)::VARCHAR OR sqlc.arg(operation_name_enable)::BOOLEAN = FALSE) AND 434 | // (start_time >= sqlc.arg(start_time_minimum)::TIMESTAMPTZ OR sqlc.arg(start_time_minimum_enable)::BOOLEAN = FALSE) AND 435 | // (start_time < sqlc.arg(start_time_maximum)::TIMESTAMPTZ OR sqlc.arg(start_time_maximum_enable)::BOOLEAN = FALSE) AND 436 | // (duration > sqlc.arg(duration_minimum)::INTERVAL OR sqlc.arg(duration_minimum_enable)::BOOLEAN = FALSE) AND 437 | // (duration < sqlc.arg(duration_maximum)::INTERVAL OR sqlc.arg(duration_maximum_enable)::BOOLEAN = FALSE) 438 | // 439 | // ; 440 | // LIMIT sqlc.arg(limit)::INT; 441 | func (q *Queries) UpsertService(ctx context.Context, name string) error { 442 | _, err := q.db.Exec(ctx, upsertService, name) 443 | return err 444 | } 445 | -------------------------------------------------------------------------------- /internal/sql/query_test.go: -------------------------------------------------------------------------------- 1 | package sql_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/jackc/pgx/v5/pgtype" 9 | "github.com/robbert229/jaeger-postgresql/internal/sql" 10 | "github.com/robbert229/jaeger-postgresql/internal/sqltest" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestGetOperations(t *testing.T) { 16 | ctx := context.Background() 17 | conn, cleanup, closer := sqltest.Harness(t) 18 | defer closer.Close() 19 | 20 | q := sql.New(conn) 21 | 22 | t.Run("should return nothing when no operations exist", func(t *testing.T) { 23 | require.Nil(t, cleanup()) 24 | 25 | err := q.UpsertService(ctx, "service-1") 26 | require.Nil(t, err) 27 | 28 | operations, err := q.GetOperations(ctx, "service-1") 29 | require.Nil(t, err) 30 | 31 | require.Empty(t, operations) 32 | }) 33 | 34 | t.Run("should not return operations from another service", func(t *testing.T) { 35 | require.Nil(t, cleanup()) 36 | 37 | err := q.UpsertService(ctx, "service-1") 38 | require.Nil(t, err) 39 | 40 | serviceID, err := q.GetServiceID(ctx, "service-1") 41 | require.Nil(t, err) 42 | 43 | err = q.UpsertOperation(ctx, sql.UpsertOperationParams{ 44 | Name: "Something", 45 | ServiceID: serviceID, 46 | Kind: sql.SpankindClient, 47 | }) 48 | require.Nil(t, err) 49 | 50 | operations, err := q.GetOperations(ctx, "service-2") 51 | require.Nil(t, err) 52 | 53 | require.Len(t, operations, 0) 54 | }) 55 | 56 | t.Run("should return something when an operation exists", func(t *testing.T) { 57 | require.Nil(t, cleanup()) 58 | 59 | err := q.UpsertService(ctx, "service-1") 60 | require.Nil(t, err) 61 | 62 | serviceID, err := q.GetServiceID(ctx, "service-1") 63 | require.Nil(t, err) 64 | 65 | err = q.UpsertOperation(ctx, sql.UpsertOperationParams{ 66 | Name: "Something", 67 | ServiceID: serviceID, 68 | Kind: sql.SpankindClient, 69 | }) 70 | require.Nil(t, err) 71 | 72 | operations, err := q.GetOperations(ctx, "service-1") 73 | require.Nil(t, err) 74 | 75 | require.Equal(t, []sql.GetOperationsRow{{Name: "Something", Kind: sql.SpankindClient}}, operations) 76 | }) 77 | } 78 | 79 | func TestGetServices(t *testing.T) { 80 | ctx := context.Background() 81 | conn, cleanup, closer := sqltest.Harness(t) 82 | defer closer.Close() 83 | 84 | q := sql.New(conn) 85 | 86 | t.Run("should return nothing when no services exist", func(t *testing.T) { 87 | require.Nil(t, cleanup()) 88 | 89 | services, err := q.GetServices(ctx) 90 | require.Nil(t, err) 91 | 92 | require.Empty(t, services) 93 | }) 94 | 95 | t.Run("should return something when an services exists", func(t *testing.T) { 96 | require.Nil(t, cleanup()) 97 | 98 | err := q.UpsertService(ctx, "Something") 99 | require.Nil(t, err) 100 | 101 | serviceID, err := q.GetServiceID(ctx, "Something") 102 | require.Nil(t, err) 103 | 104 | require.NotNil(t, serviceID) 105 | 106 | services, err := q.GetServices(ctx) 107 | require.Nil(t, err) 108 | 109 | require.Equal(t, []string{"Something"}, services) 110 | }) 111 | } 112 | 113 | func TestSpans(t *testing.T) { 114 | ctx := context.Background() 115 | conn, cleanup, closer := sqltest.Harness(t) 116 | defer closer.Close() 117 | 118 | q := sql.New(conn) 119 | 120 | t.Run("should be able to write a span", func(t *testing.T) { 121 | require.Nil(t, cleanup()) 122 | 123 | err := q.UpsertService(ctx, "service-1") 124 | require.Nil(t, err) 125 | 126 | serviceID, err := q.GetServiceID(ctx, "service-1") 127 | require.Nil(t, err) 128 | 129 | err = q.UpsertOperation(ctx, sql.UpsertOperationParams{Name: "operation-1", ServiceID: serviceID, Kind: sql.SpankindClient}) 130 | require.Nil(t, err) 131 | 132 | operationID, err := q.GetOperationID(ctx, sql.GetOperationIDParams{Name: "operation-1", ServiceID: serviceID, Kind: sql.SpankindClient}) 133 | require.Nil(t, err) 134 | 135 | _, err = q.InsertSpan(ctx, sql.InsertSpanParams{ 136 | SpanID: []byte{0, 0, 0, 1}, 137 | TraceID: []byte{0, 0, 0, 0}, 138 | OperationID: operationID, 139 | Flags: 0, 140 | StartTime: pgtype.Timestamp{Time: time.Now(), Valid: true}, 141 | Duration: pgtype.Interval{Microseconds: 1000, Valid: true}, 142 | Tags: []byte("[]"), 143 | ServiceID: serviceID, 144 | ProcessID: "", 145 | ProcessTags: []byte("[]"), 146 | Warnings: []string{}, 147 | Kind: sql.SpankindClient, 148 | Logs: []byte("null"), 149 | Refs: []byte("[]"), 150 | }) 151 | require.Nil(t, err) 152 | 153 | _, err = q.InsertSpan(ctx, sql.InsertSpanParams{ 154 | SpanID: []byte{0, 0, 0, 2}, 155 | TraceID: []byte{0, 0, 0, 0}, 156 | OperationID: operationID, 157 | Flags: 0, 158 | StartTime: pgtype.Timestamp{Time: time.Now(), Valid: true}, 159 | Duration: pgtype.Interval{Microseconds: 1000, Valid: true}, 160 | Tags: []byte("[]"), 161 | ServiceID: serviceID, 162 | ProcessID: "", 163 | ProcessTags: []byte("null"), 164 | Warnings: []string{}, 165 | Kind: sql.SpankindClient, 166 | Logs: []byte("null"), 167 | Refs: []byte("[]"), 168 | }) 169 | require.Nil(t, err) 170 | 171 | _, err = q.InsertSpan(ctx, sql.InsertSpanParams{ 172 | SpanID: []byte{0, 0, 0, 3}, 173 | TraceID: []byte{0, 0, 0, 0}, 174 | OperationID: operationID, 175 | Flags: 0, 176 | StartTime: pgtype.Timestamp{Time: time.Now(), Valid: true}, 177 | Duration: pgtype.Interval{Microseconds: 1000, Valid: true}, 178 | Tags: []byte("[]"), 179 | ServiceID: serviceID, 180 | ProcessID: "", 181 | ProcessTags: []byte("[]"), 182 | Warnings: []string{}, 183 | Kind: sql.SpankindClient, 184 | Logs: []byte("null"), 185 | Refs: []byte("[]"), 186 | }) 187 | require.Nil(t, err) 188 | 189 | queried, err := q.GetTraceSpans(ctx, []byte{0, 0, 0, 0}) 190 | require.Nil(t, err) 191 | 192 | _ = queried 193 | 194 | }) 195 | 196 | t.Run("should limit the number of traces returned according to num_traces", func(t *testing.T) { 197 | require.Nil(t, cleanup()) 198 | 199 | err := q.UpsertService(ctx, "service-1") 200 | require.Nil(t, err) 201 | 202 | serviceID, err := q.GetServiceID(ctx, "service-1") 203 | require.Nil(t, err) 204 | 205 | err = q.UpsertOperation(ctx, sql.UpsertOperationParams{Name: "operation-1", ServiceID: serviceID, Kind: sql.SpankindClient}) 206 | require.Nil(t, err) 207 | 208 | operationID, err := q.GetOperationID(ctx, sql.GetOperationIDParams{Name: "operation-1", ServiceID: serviceID, Kind: sql.SpankindClient}) 209 | require.Nil(t, err) 210 | 211 | queried, err := q.FindTraceIDs(ctx, sql.FindTraceIDsParams{NumTraces: 0}) 212 | require.Nil(t, err) 213 | 214 | require.Len(t, queried, 0) 215 | 216 | _, err = q.InsertSpan(ctx, sql.InsertSpanParams{ 217 | SpanID: []byte{0, 0, 0, 1}, 218 | TraceID: []byte{0, 0, 0, 0}, 219 | OperationID: operationID, 220 | Flags: 0, 221 | StartTime: pgtype.Timestamp{Time: time.Now(), Valid: true}, 222 | Duration: pgtype.Interval{Microseconds: 1000, Valid: true}, 223 | Tags: []byte("[]"), 224 | ServiceID: serviceID, 225 | ProcessID: "", 226 | ProcessTags: []byte("[]"), 227 | Warnings: []string{}, 228 | Kind: sql.SpankindClient, 229 | Logs: []byte("null"), 230 | Refs: []byte("[]"), 231 | }) 232 | require.Nil(t, err) 233 | 234 | _, err = q.InsertSpan(ctx, sql.InsertSpanParams{ 235 | SpanID: []byte{0, 0, 0, 0}, 236 | TraceID: []byte{0, 0, 0, 1}, 237 | OperationID: operationID, 238 | Flags: 0, 239 | StartTime: pgtype.Timestamp{Time: time.Now(), Valid: true}, 240 | Duration: pgtype.Interval{Microseconds: 1000, Valid: true}, 241 | Tags: []byte("[]"), 242 | ServiceID: serviceID, 243 | ProcessID: "", 244 | ProcessTags: []byte("null"), 245 | Warnings: []string{}, 246 | Kind: sql.SpankindClient, 247 | Logs: []byte("null"), 248 | Refs: []byte("[]"), 249 | }) 250 | require.Nil(t, err) 251 | 252 | _, err = q.InsertSpan(ctx, sql.InsertSpanParams{ 253 | SpanID: []byte{0, 0, 0, 0}, 254 | TraceID: []byte{0, 0, 0, 2}, 255 | OperationID: operationID, 256 | Flags: 0, 257 | StartTime: pgtype.Timestamp{Time: time.Now(), Valid: true}, 258 | Duration: pgtype.Interval{Microseconds: 1000, Valid: true}, 259 | Tags: []byte("[]"), 260 | ServiceID: serviceID, 261 | ProcessID: "", 262 | ProcessTags: []byte("[]"), 263 | Warnings: []string{}, 264 | Kind: sql.SpankindClient, 265 | Logs: []byte("null"), 266 | Refs: []byte("[]"), 267 | }) 268 | require.Nil(t, err) 269 | 270 | queried, err = q.FindTraceIDs(ctx, sql.FindTraceIDsParams{NumTraces: 1}) 271 | require.Nil(t, err) 272 | 273 | require.Len(t, queried, 1) 274 | 275 | queried, err = q.FindTraceIDs(ctx, sql.FindTraceIDsParams{NumTraces: 2}) 276 | require.Nil(t, err) 277 | 278 | require.Len(t, queried, 2) 279 | }) 280 | } 281 | -------------------------------------------------------------------------------- /internal/sqltest/helpers.go: -------------------------------------------------------------------------------- 1 | package sqltest 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log/slog" 8 | "time" 9 | 10 | "github.com/jackc/pgx/v5" 11 | "github.com/robbert229/jaeger-postgresql/internal/sql" 12 | "github.com/stretchr/testify/require" 13 | "github.com/testcontainers/testcontainers-go" 14 | "github.com/testcontainers/testcontainers-go/modules/postgres" 15 | "github.com/testcontainers/testcontainers-go/wait" 16 | ) 17 | 18 | func TruncateAll(conn *pgx.Conn) error { 19 | ctx := context.Background() 20 | tables := []string{"operations", "services", "spans"} 21 | for _, table := range tables { 22 | if _, err := conn.Exec(ctx, fmt.Sprintf("TRUNCATE %s CASCADE", table)); err != nil { 23 | return err 24 | } 25 | } 26 | 27 | return nil 28 | } 29 | 30 | type harnessCloser struct { 31 | *postgres.PostgresContainer 32 | *pgx.Conn 33 | } 34 | 35 | func (c harnessCloser) Close() error { 36 | err := c.Conn.Close(context.Background()) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | err = c.Terminate(context.Background()) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | return nil 47 | } 48 | 49 | // Harness provides a test harness 50 | func Harness(t interface { 51 | Errorf(format string, args ...interface{}) 52 | FailNow() 53 | Helper() 54 | }) (*pgx.Conn, func() error, io.Closer) { 55 | t.Helper() 56 | 57 | dbName := "jaeger" 58 | dbUser := "postgres" 59 | dbPassword := "password" 60 | 61 | ctx := context.Background() 62 | pgC, err := postgres.RunContainer(ctx, 63 | testcontainers.WithImage("postgres:15.2-alpine"), 64 | postgres.WithDatabase(dbName), 65 | postgres.WithUsername(dbUser), 66 | postgres.WithPassword(dbPassword), 67 | testcontainers.WithWaitStrategy( 68 | wait.ForLog("database system is ready to accept connections"). 69 | WithOccurrence(2). 70 | WithStartupTimeout(5*time.Second), 71 | ), 72 | ) 73 | require.Nil(t, err, "failed to get testcontainer") 74 | 75 | endpoint, err := pgC.Endpoint(ctx, "") 76 | require.Nil(t, err, "failed to get endpoint") 77 | 78 | databaseURL := "postgres://postgres:password@" + endpoint + "/postgres" 79 | 80 | err = sql.Migrate(slog.Default(), databaseURL) 81 | require.Nil(t, err, "failed to migrate database") 82 | 83 | conn, err := pgx.Connect(ctx, databaseURL) 84 | require.Nil(t, err, "failed to connect to database") 85 | 86 | return conn, func() error { 87 | return TruncateAll(conn) 88 | }, harnessCloser{pgC, conn} 89 | } 90 | -------------------------------------------------------------------------------- /internal/store/instrumentation.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "time" 7 | 8 | "github.com/jaegertracing/jaeger/model" 9 | "github.com/jaegertracing/jaeger/storage/spanstore" 10 | "github.com/prometheus/client_golang/prometheus" 11 | "github.com/prometheus/client_golang/prometheus/promauto" 12 | ) 13 | 14 | const ( 15 | promNamespace = "jaeger_postgresql" 16 | ) 17 | 18 | // reader 19 | 20 | var ( 21 | 22 | // GetTrace 23 | promGetTraceCounter = promauto.NewCounter(prometheus.CounterOpts{ 24 | Namespace: promNamespace, 25 | Name: "get_trace_total", 26 | Help: "The total number of calls to GetTrace", 27 | }) 28 | 29 | promGetTraceHistogram = promauto.NewHistogram(prometheus.HistogramOpts{ 30 | Namespace: promNamespace, 31 | Name: "get_trace_seconds", 32 | Help: "The time spent in GetTrace", 33 | }) 34 | 35 | promGetTraceErrorsCounter = promauto.NewCounter(prometheus.CounterOpts{ 36 | Namespace: promNamespace, 37 | Name: "get_trace_errors_total", 38 | Help: "The total number of errors returned from GetTrace", 39 | }) 40 | 41 | // FindTraces 42 | promFindTracesCounter = promauto.NewCounter(prometheus.CounterOpts{ 43 | Namespace: promNamespace, 44 | Name: "find_traces_total", 45 | Help: "The total number of calls to FindTraces", 46 | }) 47 | 48 | promFindTracesHistogram = promauto.NewHistogram(prometheus.HistogramOpts{ 49 | Namespace: promNamespace, 50 | Name: "find_traces_seconds", 51 | Help: "The time spent in FindTraces", 52 | }) 53 | 54 | promFindTracesErrorsCounter = promauto.NewCounter(prometheus.CounterOpts{ 55 | Namespace: promNamespace, 56 | Name: "find_traces_errors_total", 57 | Help: "The total number of errors returned from FindTraces", 58 | }) 59 | 60 | // FindTraceIDs 61 | promFindTraceIDsCounter = promauto.NewCounter(prometheus.CounterOpts{ 62 | Namespace: promNamespace, 63 | Name: "find_trace_ids_total", 64 | Help: "The total number of calls to FindTraceIDs", 65 | }) 66 | 67 | promFindTraceIDsHistogram = promauto.NewHistogram(prometheus.HistogramOpts{ 68 | Namespace: promNamespace, 69 | Name: "find_trace_ids_seconds", 70 | Help: "The time spent in FindTraceIDs", 71 | }) 72 | 73 | promFindTraceIDsErrorsCounter = promauto.NewCounter(prometheus.CounterOpts{ 74 | Namespace: promNamespace, 75 | Name: "find_trace_ids_errors_total", 76 | Help: "The total number of errors for FindTraceIDs", 77 | }) 78 | ) 79 | 80 | // writer 81 | 82 | var ( 83 | // WriteSpan 84 | promWriteSpanCounter = promauto.NewCounter(prometheus.CounterOpts{ 85 | Namespace: promNamespace, 86 | Name: "write_span_total", 87 | Help: "The total number of calls to WriteSpan", 88 | }) 89 | 90 | promWriteSpanHistogram = promauto.NewHistogram(prometheus.HistogramOpts{ 91 | Namespace: promNamespace, 92 | Name: "write_span_seconds", 93 | Help: "The time spent in WriteSpans", 94 | }) 95 | 96 | promWriteSpanErrorsCounter = promauto.NewCounter(prometheus.CounterOpts{ 97 | Namespace: promNamespace, 98 | Name: "write_span_errors_total", 99 | Help: "The total number of errors returned from WriteSpan", 100 | }) 101 | ) 102 | 103 | // NewInstrumentedWriter returns a new spanstore.Writer that is instrumented. 104 | func NewInstrumentedWriter(embedded spanstore.Writer, logger *slog.Logger) *InstrumentedWriter { 105 | return &InstrumentedWriter{Writer: embedded, logger: logger} 106 | } 107 | 108 | // InstrumentedWriter is a writer that has been instrumented. 109 | type InstrumentedWriter struct { 110 | spanstore.Writer 111 | logger *slog.Logger 112 | } 113 | 114 | func (w InstrumentedWriter) WriteSpan(ctx context.Context, span *model.Span) error { 115 | // instrumentation preamble 116 | { 117 | promWriteSpanCounter.Inc() 118 | 119 | start := time.Now() 120 | defer func() { 121 | promWriteSpanHistogram.Observe(time.Since(start).Seconds()) 122 | }() 123 | 124 | w.logger.Debug( 125 | "inserting span", 126 | "span", span.SpanID, 127 | "trace_id", span.TraceID, 128 | "operation_name", span.OperationName, 129 | ) 130 | } 131 | 132 | err := w.Writer.WriteSpan(ctx, span) 133 | if err != nil { 134 | promWriteSpanErrorsCounter.Inc() 135 | w.logger.Error("failed to write span", "err", err) 136 | return err 137 | } 138 | 139 | return nil 140 | } 141 | 142 | // NewInstrumentedReader returns a new spanstore.Reader that is instrumented. 143 | func NewInstrumentedReader(embedded spanstore.Reader, logger *slog.Logger) *InstrumentedReader { 144 | return &InstrumentedReader{Reader: embedded, logger: logger} 145 | } 146 | 147 | // InstrumentedReader is a reader that has been instrumented. 148 | type InstrumentedReader struct { 149 | spanstore.Reader 150 | logger *slog.Logger 151 | } 152 | 153 | // GetTrace takes a traceID and returns a Trace associated with that traceID 154 | func (r *InstrumentedReader) GetTrace(ctx context.Context, traceID model.TraceID) (*model.Trace, error) { 155 | { 156 | promGetTraceCounter.Inc() 157 | 158 | start := time.Now() 159 | defer func() { 160 | promGetTraceHistogram.Observe(time.Since(start).Seconds()) 161 | }() 162 | } 163 | 164 | trace, err := r.Reader.GetTrace(ctx, traceID) 165 | if err != nil { 166 | promGetTraceErrorsCounter.Inc() 167 | r.logger.Error("failed to get trace", "err", err) 168 | return nil, err 169 | } 170 | 171 | return trace, nil 172 | } 173 | 174 | // FindTraces retrieve traces that match the traceQuery 175 | func (r *InstrumentedReader) FindTraces(ctx context.Context, query *spanstore.TraceQueryParameters) ([]*model.Trace, error) { 176 | { 177 | promFindTracesCounter.Inc() 178 | 179 | start := time.Now() 180 | defer func() { 181 | promFindTracesHistogram.Observe(time.Since(start).Seconds()) 182 | }() 183 | } 184 | 185 | traces, err := r.Reader.FindTraces(ctx, query) 186 | if err != nil { 187 | promFindTracesErrorsCounter.Inc() 188 | r.logger.Error("failed to find traces", "err", err) 189 | return nil, err 190 | } 191 | 192 | return traces, nil 193 | } 194 | 195 | // FindTraceIDs retrieve traceIDs that match the traceQuery 196 | func (r *InstrumentedReader) FindTraceIDs(ctx context.Context, query *spanstore.TraceQueryParameters) ([]model.TraceID, error) { 197 | { 198 | promFindTraceIDsCounter.Inc() 199 | 200 | start := time.Now() 201 | defer func() { 202 | promFindTraceIDsErrorsCounter.Inc() 203 | promFindTraceIDsHistogram.Observe(time.Since(start).Seconds()) 204 | }() 205 | } 206 | 207 | traceIDs, err := r.Reader.FindTraceIDs(ctx, query) 208 | if err != nil { 209 | r.logger.Error("failed to retrieve trace ids", "err", err) 210 | return nil, err 211 | } 212 | 213 | return traceIDs, nil 214 | } 215 | -------------------------------------------------------------------------------- /internal/store/integration_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "testing" 7 | "time" 8 | 9 | "github.com/robbert229/jaeger-postgresql/internal/sql" 10 | "github.com/robbert229/jaeger-postgresql/internal/sqltest" 11 | 12 | "github.com/stretchr/testify/require" 13 | 14 | "github.com/jaegertracing/jaeger/model" 15 | jaeger_integration_tests "github.com/jaegertracing/jaeger/plugin/storage/integration" 16 | "github.com/jaegertracing/jaeger/storage/spanstore" 17 | ) 18 | 19 | func TestJaegerStorageIntegration(t *testing.T) { 20 | conn, cleanup, closer := sqltest.Harness(t) 21 | defer closer.Close() 22 | 23 | require.Nil(t, cleanup()) 24 | 25 | q := sql.New(conn) 26 | 27 | logger := slog.Default() 28 | reader := NewReader(q, logger.With("component", "reader")) 29 | writer := NewWriter(q, logger.With("component", "writer")) 30 | si := jaeger_integration_tests.StorageIntegration{ 31 | SpanReader: reader, 32 | SpanWriter: writer, 33 | DependencyReader: reader, 34 | GetDependenciesReturnsSource: false, 35 | CleanUp: cleanup, 36 | Refresh: func() error { return nil }, 37 | SkipList: []string{}, 38 | } 39 | 40 | // Runs all storage integration tests. 41 | si.IntegrationTestAll(t) 42 | } 43 | 44 | func TestSpans(t *testing.T) { 45 | conn, cleanup, closer := sqltest.Harness(t) 46 | defer closer.Close() 47 | 48 | require.Nil(t, cleanup()) 49 | 50 | ctx := context.Background() 51 | 52 | q := sql.New(conn) 53 | 54 | logger := slog.Default() 55 | w := NewWriter(q, logger) 56 | r := NewReader(q, logger) 57 | 58 | ts := TruncateTime(time.Now()) 59 | 60 | span := &model.Span{ 61 | TraceID: model.NewTraceID(0, 0), 62 | SpanID: model.NewSpanID(0), 63 | OperationName: "operation", 64 | Process: model.NewProcess("service", []model.KeyValue{model.Bool("foo", true)}), 65 | Logs: []model.Log{{Timestamp: ts, Fields: []model.KeyValue{model.Bool("foo", false)}}}, 66 | Tags: []model.KeyValue{model.Bool("fizzbuzz", true)}, 67 | References: []model.SpanRef{}, 68 | } 69 | 70 | err := w.WriteSpan(ctx, span) 71 | require.Nil(t, err) 72 | 73 | trace, err := r.FindTraces(ctx, &spanstore.TraceQueryParameters{ 74 | ServiceName: "service", 75 | OperationName: "operation", 76 | NumTraces: 100, 77 | }) 78 | require.Nil(t, err) 79 | 80 | require.Len(t, trace, 1) 81 | require.Equal(t, span, trace[0].Spans[0]) 82 | } 83 | -------------------------------------------------------------------------------- /internal/store/mapping.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/binary" 6 | "encoding/json" 7 | "fmt" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/robbert229/jaeger-postgresql/internal/sql" 12 | 13 | "github.com/jackc/pgx/v5/pgtype" 14 | "github.com/jaegertracing/jaeger/model" 15 | "go.opentelemetry.io/otel/trace" 16 | ) 17 | 18 | // DecodeTraceID converts a slice of raw bytes into a trace id. 19 | func DecodeTraceID(raw []byte) model.TraceID { 20 | low := binary.LittleEndian.Uint64(raw[0:8]) 21 | high := binary.LittleEndian.Uint64(raw[8:16]) 22 | return model.NewTraceID(low, high) 23 | } 24 | 25 | // EncodeTraceID converts a trace id to a slice of raw bytes. 26 | func EncodeTraceID(traceID model.TraceID) []byte { 27 | raw := []byte{} 28 | raw = binary.LittleEndian.AppendUint64(raw, traceID.High) 29 | raw = binary.LittleEndian.AppendUint64(raw, traceID.Low) 30 | return raw 31 | } 32 | 33 | // EncodeSpanID encodes a span id into a slice of bytes. 34 | func EncodeSpanID(spanID model.SpanID) []byte { 35 | return binary.LittleEndian.AppendUint64(nil, uint64(spanID)) 36 | } 37 | 38 | // DecodeSpanID decodes a span id form a byte slice. 39 | func DecodeSpanID(raw []byte) model.SpanID { 40 | return model.NewSpanID(binary.LittleEndian.Uint64(raw)) 41 | } 42 | 43 | func EncodeInterval(duration time.Duration) pgtype.Interval { 44 | return pgtype.Interval{Microseconds: duration.Microseconds(), Valid: true} 45 | } 46 | 47 | func EncodeTimestamp(t time.Time) pgtype.Timestamp { 48 | return pgtype.Timestamp{Time: t, Valid: true} 49 | } 50 | 51 | func encodeTagsToSlice(input []model.KeyValue) [][]any { 52 | slice := make([][]any, len(input)) 53 | 54 | for i, kv := range input { 55 | var value interface{} 56 | if kv.VType == model.ValueType_STRING { 57 | value = kv.VStr 58 | } else if kv.VType == model.ValueType_BOOL { 59 | value = kv.VBool 60 | } else if kv.VType == model.ValueType_INT64 { 61 | value = fmt.Sprintf("%d", kv.VInt64) 62 | } else if kv.VType == model.ValueType_FLOAT64 { 63 | value = kv.VFloat64 64 | } else if kv.VType == model.ValueType_BINARY { 65 | value = base64.RawStdEncoding.EncodeToString(kv.VBinary) 66 | } 67 | 68 | slice[i] = []any{ 69 | kv.Key, 70 | kv.VType, 71 | value, 72 | } 73 | } 74 | 75 | return slice 76 | } 77 | 78 | func EncodeTags(input []model.KeyValue) ([]byte, error) { 79 | slice := encodeTagsToSlice(input) 80 | 81 | bytes, err := json.Marshal(slice) 82 | if err != nil { 83 | return nil, fmt.Errorf("failed to encode to json: %w", err) 84 | } 85 | 86 | return bytes, nil 87 | } 88 | 89 | func decodeTagsFromSlice(slice []any) ([]model.KeyValue, error) { 90 | var tags []model.KeyValue 91 | for _, subslice := range slice { 92 | cast := subslice.([]any) 93 | 94 | key := cast[0].(string) 95 | vType := model.ValueType(int(cast[1].(float64))) 96 | value := cast[2] 97 | 98 | kv := model.KeyValue{Key: key, VType: vType} 99 | switch vType { 100 | case model.StringType: 101 | kv.VStr = value.(string) 102 | case model.BoolType: 103 | kv.VBool = value.(bool) 104 | case model.Int64Type: 105 | parsed, err := strconv.ParseInt(value.(string), 10, 64) 106 | if err != nil { 107 | return nil, fmt.Errorf("failed to parse int: %w", err) 108 | } 109 | 110 | kv.VInt64 = parsed 111 | case model.Float64Type: 112 | kv.VFloat64 = value.(float64) 113 | case model.BinaryType: 114 | bytes, err := base64.RawStdEncoding.DecodeString(value.(string)) 115 | if err != nil { 116 | return nil, fmt.Errorf("failed to parse: %w", err) 117 | } 118 | 119 | kv.VBinary = bytes 120 | } 121 | 122 | tags = append(tags, kv) 123 | } 124 | 125 | // tags must not be nil. This is because the serialization/deserialization 126 | // tests demand it to be empty array 127 | if tags == nil { 128 | tags = []model.KeyValue{} 129 | } 130 | 131 | return tags, nil 132 | } 133 | 134 | func DecodeTags(input []byte) ([]model.KeyValue, error) { 135 | slice := []any{} 136 | if err := json.Unmarshal(input, &slice); err != nil { 137 | return nil, fmt.Errorf("failed to decode tag json: %w", err) 138 | } 139 | 140 | tags, err := decodeTagsFromSlice(slice) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | return tags, nil 146 | 147 | } 148 | 149 | func EncodeSpanKind(modelKind trace.SpanKind) sql.Spankind { 150 | switch modelKind { 151 | case trace.SpanKindClient: 152 | return sql.SpankindClient 153 | case trace.SpanKindServer: 154 | return sql.SpankindServer 155 | case trace.SpanKindProducer: 156 | return sql.SpankindProducer 157 | case trace.SpanKindConsumer: 158 | return sql.SpankindConsumer 159 | case trace.SpanKindInternal: 160 | return sql.SpankindInternal 161 | case trace.SpanKindUnspecified: 162 | return sql.SpankindUnspecified 163 | default: 164 | return sql.SpankindUnspecified 165 | } 166 | } 167 | 168 | func EncodeLogs(logs []model.Log) ([]byte, error) { 169 | slice := make([][]any, len(logs)) 170 | for i, log := range logs { 171 | 172 | slice[i] = []any{ 173 | pgtype.Timestamp{Time: log.Timestamp, Valid: true}, 174 | encodeTagsToSlice(log.Fields), 175 | } 176 | } 177 | 178 | bytes, err := json.Marshal(slice) 179 | if err != nil { 180 | return nil, err 181 | } 182 | 183 | return bytes, nil 184 | } 185 | 186 | func TruncateTime(ts time.Time) time.Time { 187 | ts, err := time.Parse(time.RFC3339Nano, ts.Format(time.RFC3339Nano)) 188 | if err != nil { 189 | panic(err) 190 | } 191 | 192 | return ts 193 | } 194 | 195 | func DecodeLogs(raw []byte) ([]model.Log, error) { 196 | slice := [][]any{} 197 | if err := json.Unmarshal(raw, &slice); err != nil { 198 | return nil, fmt.Errorf("failed to decode logs json: %w", err) 199 | } 200 | 201 | logs := make([]model.Log, len(slice)) 202 | for i, subslice := range slice { 203 | cast := subslice[1].([]any) 204 | fields, err := decodeTagsFromSlice(cast) 205 | if err != nil { 206 | return nil, err 207 | } 208 | 209 | layout := time.RFC3339Nano 210 | t, err := time.Parse(layout, subslice[0].(string)) 211 | if err != nil { 212 | return nil, err 213 | } 214 | 215 | logs[i] = model.Log{ 216 | Timestamp: t, 217 | Fields: fields, 218 | } 219 | } 220 | 221 | return logs, nil 222 | } 223 | 224 | func EncodeSpanRefs(spanrefs []model.SpanRef) ([]byte, error) { 225 | if len(spanrefs) == 0 { 226 | return []byte("[]"), nil 227 | } 228 | 229 | slice := make([][]any, len(spanrefs)) 230 | for i, spanref := range spanrefs { 231 | slice[i] = []any{ 232 | base64.StdEncoding.EncodeToString(EncodeTraceID(spanref.TraceID)), 233 | base64.StdEncoding.EncodeToString(EncodeSpanID(spanref.SpanID)), 234 | int32(spanref.RefType), 235 | } 236 | } 237 | bytes, err := json.Marshal(slice) 238 | if err != nil { 239 | return nil, err 240 | } 241 | 242 | return bytes, nil 243 | } 244 | 245 | func DecodeSpanRefs(data []byte) ([]model.SpanRef, error) { 246 | var slice [][]any 247 | err := json.Unmarshal(data, &slice) 248 | if err != nil { 249 | return nil, err 250 | } 251 | 252 | results := make([]model.SpanRef, len(slice)) 253 | 254 | for i, subslice := range slice { 255 | traceID, err := base64.StdEncoding.DecodeString(subslice[0].(string)) 256 | if err != nil { 257 | return nil, err 258 | } 259 | 260 | spanID, err := base64.StdEncoding.DecodeString(subslice[1].(string)) 261 | if err != nil { 262 | return nil, err 263 | } 264 | 265 | results[i] = model.SpanRef{ 266 | TraceID: DecodeTraceID(traceID), 267 | SpanID: DecodeSpanID(spanID), 268 | RefType: model.SpanRefType(int32(subslice[2].(float64))), 269 | } 270 | } 271 | 272 | return results, nil 273 | } 274 | -------------------------------------------------------------------------------- /internal/store/mapping_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/jaegertracing/jaeger/model" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestToDomainTraceID(t *testing.T) { 12 | traceID := model.NewTraceID(127318, 12489421) 13 | 14 | encoded := EncodeTraceID(traceID) 15 | decoded := DecodeTraceID(encoded) 16 | 17 | require.Equal(t, decoded, traceID) 18 | } 19 | -------------------------------------------------------------------------------- /internal/store/reader.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "time" 8 | 9 | "github.com/robbert229/jaeger-postgresql/internal/sql" 10 | 11 | "github.com/jaegertracing/jaeger/model" 12 | "github.com/jaegertracing/jaeger/storage/spanstore" 13 | ) 14 | 15 | var _ spanstore.Reader = (*Reader)(nil) 16 | 17 | // Reader can query for and load traces from PostgreSQL v2.x. 18 | type Reader struct { 19 | logger *slog.Logger 20 | q *sql.Queries 21 | } 22 | 23 | // NewReader returns a new SpanReader for PostgreSQL v2.x. 24 | func NewReader(q *sql.Queries, logger *slog.Logger) *Reader { 25 | return &Reader{ 26 | q: q, 27 | logger: logger, 28 | } 29 | } 30 | 31 | // GetServices returns all services traced by Jaeger 32 | func (r *Reader) GetServices(ctx context.Context) ([]string, error) { 33 | services, err := r.q.GetServices(ctx) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return services, nil 39 | } 40 | 41 | // GetOperations returns all operations for a specific service traced by Jaeger 42 | func (r *Reader) GetOperations(ctx context.Context, param spanstore.OperationQueryParameters) ([]spanstore.Operation, error) { 43 | response, err := r.q.GetOperations(ctx, param.ServiceName) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | var operations = make([]spanstore.Operation, len(response)) 49 | for i, iter := range response { 50 | operations[i] = spanstore.Operation{Name: iter.Name, SpanKind: string(iter.Kind)} 51 | } 52 | 53 | return operations, nil 54 | } 55 | 56 | // GetTrace takes a traceID and returns a Trace associated with that traceID 57 | func (r *Reader) GetTrace(ctx context.Context, traceID model.TraceID) (*model.Trace, error) { 58 | { 59 | promGetTraceCounter.Inc() 60 | 61 | start := time.Now() 62 | defer func() { 63 | promGetTraceHistogram.Observe(time.Since(start).Seconds()) 64 | }() 65 | } 66 | 67 | dbSpans, err := r.q.GetTraceSpans(ctx, EncodeTraceID(traceID)) 68 | if err != nil { 69 | return nil, fmt.Errorf("failed to get trace spans: %w", err) 70 | } 71 | 72 | if len(dbSpans) == 0 { 73 | return nil, fmt.Errorf("trace not found") 74 | } 75 | 76 | var spans []*model.Span = make([]*model.Span, len(dbSpans)) 77 | for i, dbSpan := range dbSpans { 78 | tags, err := DecodeTags(dbSpan.Tags) 79 | if err != nil { 80 | return nil, fmt.Errorf("failed to decode span tags: %w", err) 81 | } 82 | 83 | processTags, err := DecodeTags(dbSpan.ProcessTags) 84 | if err != nil { 85 | return nil, fmt.Errorf("failed to decode process tags: %w", err) 86 | } 87 | 88 | duration := time.Duration(dbSpan.Duration.Microseconds * 1000) 89 | 90 | logs, err := DecodeLogs(dbSpan.Logs) 91 | if err != nil { 92 | return nil, fmt.Errorf("failed to decode logs: %w", err) 93 | } 94 | 95 | decodedSpanRefs, err := DecodeSpanRefs(dbSpan.Refs) 96 | if err != nil { 97 | return nil, fmt.Errorf("failed to decode spanrefs: %w", err) 98 | } 99 | 100 | spans[i] = &model.Span{ 101 | TraceID: DecodeTraceID(dbSpan.TraceID), 102 | SpanID: DecodeSpanID(dbSpan.SpanID), 103 | OperationName: dbSpan.OperationName, 104 | Tags: tags, 105 | References: decodedSpanRefs, 106 | Flags: model.Flags(int32(dbSpan.Flags)), 107 | StartTime: dbSpan.StartTime.Time, 108 | Duration: duration, 109 | Logs: logs, 110 | Process: &model.Process{ 111 | ServiceName: dbSpan.ProcessName, 112 | Tags: processTags, 113 | }, 114 | ProcessID: dbSpan.ProcessID, 115 | Warnings: dbSpan.Warnings, 116 | } 117 | } 118 | 119 | return &model.Trace{ 120 | Spans: spans, 121 | }, nil 122 | } 123 | 124 | // FindTraces retrieve traces that match the traceQuery 125 | func (r *Reader) FindTraces(ctx context.Context, query *spanstore.TraceQueryParameters) ([]*model.Trace, error) { 126 | { 127 | promFindTracesCounter.Inc() 128 | 129 | start := time.Now() 130 | defer func() { 131 | promFindTracesHistogram.Observe(time.Since(start).Seconds()) 132 | }() 133 | } 134 | 135 | response, err := r.q.FindTraceIDs(ctx, sql.FindTraceIDsParams{ 136 | ServiceName: query.ServiceName, 137 | ServiceNameEnableFilter: len(query.ServiceName) > 0, 138 | OperationName: query.OperationName, 139 | OperationNameEnableFilter: len(query.OperationName) > 0, 140 | StartTimeMinimum: EncodeTimestamp(query.StartTimeMin), 141 | StartTimeMinimumEnableFilter: query.StartTimeMin.After(time.Time{}), 142 | StartTimeMaximum: EncodeTimestamp(query.StartTimeMax), 143 | StartTimeMaximumEnableFilter: query.StartTimeMax.After(time.Time{}), 144 | DurationMinimum: EncodeInterval(query.DurationMin), 145 | DurationMinimumEnableFilter: query.DurationMin != time.Duration(0), 146 | DurationMaximum: EncodeInterval(query.DurationMax), 147 | DurationMaximumEnableFilter: query.DurationMax != time.Duration(0), 148 | NumTraces: int32(query.NumTraces), 149 | // Tags 150 | }) 151 | if err != nil { 152 | return nil, fmt.Errorf("failed to query trace ids: %w", err) 153 | } 154 | 155 | var traces []*model.Trace 156 | for _, id := range response { 157 | trace, err := r.GetTrace(ctx, DecodeTraceID(id)) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | traces = append(traces, trace) 163 | } 164 | 165 | return traces, nil 166 | } 167 | 168 | // FindTraceIDs retrieve traceIDs that match the traceQuery 169 | func (r *Reader) FindTraceIDs(ctx context.Context, query *spanstore.TraceQueryParameters) ([]model.TraceID, error) { 170 | { 171 | promFindTraceIDsCounter.Inc() 172 | 173 | start := time.Now() 174 | defer func() { 175 | promFindTraceIDsHistogram.Observe(time.Since(start).Seconds()) 176 | }() 177 | } 178 | 179 | response, err := r.q.FindTraceIDs(ctx, sql.FindTraceIDsParams{ 180 | ServiceName: query.ServiceName, 181 | ServiceNameEnableFilter: len(query.ServiceName) > 0, 182 | OperationName: query.OperationName, 183 | OperationNameEnableFilter: len(query.OperationName) > 0, 184 | StartTimeMinimum: EncodeTimestamp(query.StartTimeMin), 185 | StartTimeMinimumEnableFilter: query.StartTimeMin.After(time.Time{}), 186 | StartTimeMaximum: EncodeTimestamp(query.StartTimeMax), 187 | StartTimeMaximumEnableFilter: query.StartTimeMax.After(time.Time{}), 188 | DurationMinimum: EncodeInterval(query.DurationMin), 189 | DurationMinimumEnableFilter: query.DurationMin > 0*time.Second, 190 | DurationMaximum: EncodeInterval(query.DurationMax), 191 | DurationMaximumEnableFilter: query.DurationMax > 0*time.Second, 192 | // TODO(johnrowl) add tags 193 | NumTraces: int32(query.NumTraces), 194 | }) 195 | if err != nil { 196 | return nil, fmt.Errorf("failed to query trace ids: %w", err) 197 | } 198 | 199 | var traceIDs = make([]model.TraceID, len(response)) 200 | for i, iter := range response { 201 | traceIDs[i] = DecodeTraceID(iter) 202 | } 203 | 204 | return traceIDs, nil 205 | } 206 | 207 | // GetDependencies returns all inter-service dependencies 208 | func (r *Reader) GetDependencies(ctx context.Context, endTs time.Time, lookback time.Duration) ([]model.DependencyLink, error) { 209 | return []model.DependencyLink{}, nil 210 | } 211 | -------------------------------------------------------------------------------- /internal/store/writer.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log/slog" 8 | 9 | "github.com/robbert229/jaeger-postgresql/internal/sql" 10 | 11 | "go.opentelemetry.io/otel/trace" 12 | 13 | "github.com/jaegertracing/jaeger/model" 14 | "github.com/jaegertracing/jaeger/storage/spanstore" 15 | ) 16 | 17 | var _ spanstore.Writer = (*Writer)(nil) 18 | var _ io.Closer = (*Writer)(nil) 19 | 20 | // Writer handles all writes to PostgreSQL 2.x for the Jaeger data model 21 | type Writer struct { 22 | q *sql.Queries 23 | logger *slog.Logger 24 | } 25 | 26 | // NewWriter returns a Writer. 27 | func NewWriter(q *sql.Queries, logger *slog.Logger) *Writer { 28 | w := &Writer{ 29 | q: q, 30 | logger: logger, 31 | } 32 | 33 | return w 34 | } 35 | 36 | // Close triggers a graceful shutdown 37 | func (w *Writer) Close() error { 38 | return nil 39 | } 40 | 41 | // WriteSpan saves the span into PostgreSQL 42 | func (w *Writer) WriteSpan(ctx context.Context, span *model.Span) error { 43 | err := w.q.UpsertService(ctx, span.Process.ServiceName) 44 | if err != nil { 45 | return fmt.Errorf("failed to upsert span service: %w", err) 46 | } 47 | 48 | serviceID, err := w.q.GetServiceID(ctx, span.Process.ServiceName) 49 | if err != nil { 50 | return fmt.Errorf("failed to get service id: %w", err) 51 | } 52 | 53 | modelKind, ok := span.GetSpanKind() 54 | if !ok { 55 | modelKind = trace.SpanKindUnspecified 56 | } 57 | 58 | err = w.q.UpsertOperation(ctx, sql.UpsertOperationParams{ 59 | Name: span.OperationName, 60 | ServiceID: serviceID, 61 | Kind: EncodeSpanKind(modelKind), 62 | }) 63 | if err != nil { 64 | return fmt.Errorf("failed to upsert span operation: %w", err) 65 | } 66 | 67 | operationID, err := w.q.GetOperationID(ctx, sql.GetOperationIDParams{ 68 | Name: span.OperationName, 69 | ServiceID: serviceID, 70 | Kind: EncodeSpanKind(modelKind), 71 | }) 72 | if err != nil { 73 | return fmt.Errorf("failed to get operation id: %w", err) 74 | } 75 | 76 | logs, err := EncodeLogs(span.Logs) 77 | if err != nil { 78 | return fmt.Errorf("failed to encode logs: %w", err) 79 | } 80 | 81 | tags, err := EncodeTags(span.Tags) 82 | if err != nil { 83 | return fmt.Errorf("failed to encode tags: %w", err) 84 | } 85 | 86 | processTags, err := EncodeTags(span.Process.Tags) 87 | if err != nil { 88 | return fmt.Errorf("failed to encode process tags: %w", err) 89 | } 90 | 91 | encodedSpanRefs, err := EncodeSpanRefs(span.References) 92 | if err != nil { 93 | return fmt.Errorf("failed to encode spanrefs: %w", err) 94 | } 95 | 96 | _, err = w.q.InsertSpan(ctx, sql.InsertSpanParams{ 97 | SpanID: EncodeSpanID(span.SpanID), 98 | TraceID: EncodeTraceID(span.TraceID), 99 | OperationID: operationID, 100 | Flags: int64(span.Flags), 101 | StartTime: EncodeTimestamp(span.StartTime), 102 | Duration: EncodeInterval(span.Duration), 103 | Tags: tags, 104 | ServiceID: serviceID, 105 | ProcessID: span.ProcessID, 106 | Warnings: span.Warnings, 107 | ProcessTags: processTags, 108 | Kind: EncodeSpanKind(modelKind), 109 | Logs: logs, 110 | Refs: encodedSpanRefs, 111 | }) 112 | if err != nil { 113 | return fmt.Errorf("failed to insert span: %w", err) 114 | } 115 | 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", 3 | "packages": { 4 | ".": { 5 | "changelog-path": "CHANGELOG.md", 6 | "release-type": "simple", 7 | "bump-minor-pre-major": false, 8 | "bump-patch-for-minor-pre-major": false, 9 | "draft": false, 10 | "prerelease": false, 11 | "extra-files": [ 12 | "README.md", 13 | "Makefile" 14 | ] 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | sql: 3 | - engine: "postgresql" 4 | queries: "internal/sql/query.sql" 5 | schema: "internal/sql/migrations/001_initial.sql" 6 | gen: 7 | go: 8 | out: "internal/sql" 9 | sql_package: "pgx/v5" --------------------------------------------------------------------------------