├── .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 | 
4 | 
5 | [](https://github.com/robbert229/jaeger-postresql/stargazers)
6 | 
7 | 
8 | 
9 | 
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 |
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"
--------------------------------------------------------------------------------