Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffaf6a7f27 | ||
|
|
a16f09de4a | ||
|
|
2ef8ccb6ee | ||
|
|
26d4199330 | ||
|
|
b32c8c88d2 | ||
|
|
6a3256c9b6 | ||
|
|
8a5c8dcdb7 | ||
|
|
92db06698c | ||
|
|
8b3d3110fe | ||
|
|
80d749ab92 | ||
|
|
88dde56687 | ||
|
|
e9a2dcf720 | ||
|
|
e824f8b5b9 | ||
|
|
d8ed1a9bf8 | ||
|
|
d1e99a1f1f | ||
|
|
52ba172550 | ||
|
|
5b128fb3e2 | ||
|
|
9b04b1b897 | ||
|
|
d0b6302893 | ||
|
|
fdb17a58c8 | ||
|
|
ece0c99dc3 | ||
|
|
81231179c8 | ||
|
|
48c718a183 | ||
|
|
08962365dc | ||
|
|
c41b3ea911 | ||
|
|
c436c8219c | ||
|
|
4524b6dba8 | ||
|
|
8c9e9e3876 | ||
|
|
3f0d9eff99 | ||
|
|
4732aa5ab2 | ||
|
|
c00564cdcb | ||
|
|
2836af0242 | ||
|
|
212369811f | ||
|
|
afac050279 | ||
|
|
afa847c76c | ||
|
|
fff76c4acc | ||
|
|
7bc3101cee | ||
|
|
a8a8d53cd1 | ||
|
|
485b8434d8 | ||
|
|
9dce38187c | ||
|
|
33f22237dc | ||
|
|
4bbd8b3e63 | ||
|
|
ec0ea2a287 | ||
|
|
dc4bc4a98c | ||
|
|
e92f853311 | ||
|
|
a6660fd70d | ||
|
|
cbf01d4546 | ||
|
|
cfab683ab8 | ||
|
|
4a6c014206 | ||
|
|
88ae10c9d7 | ||
|
|
f1cfd58922 | ||
|
|
d5528d3635 |
No files matched your search
+15
-19
@@ -10,19 +10,18 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
go-version: 'stable'
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: go test -race -coverprofile=coverage.txt ./...
|
run: go test -race -coverprofile=coverage.txt ./...
|
||||||
|
|
||||||
- name: Check coverage
|
- name: Check coverage
|
||||||
uses: vladopajic/go-test-coverage@v2
|
id: coverage
|
||||||
with:
|
run: |
|
||||||
config: ./.testcoverage.yml
|
go install github.com/vladopajic/go-test-coverage/v2@latest
|
||||||
|
go-test-coverage --config ./.testcoverage.yml --github-action-output
|
||||||
# Download baseline coverage from main branch (for PRs)
|
|
||||||
- name: Download baseline coverage
|
- name: Download baseline coverage
|
||||||
if: gitea.event_name == 'pull_request'
|
if: gitea.event_name == 'pull_request'
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
@@ -30,14 +29,12 @@ jobs:
|
|||||||
name: coverage-baseline
|
name: coverage-baseline
|
||||||
path: ./baseline
|
path: ./baseline
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
# Compare coverage against baseline (for PRs)
|
|
||||||
- name: Compare coverage
|
- name: Compare coverage
|
||||||
if: gitea.event_name == 'pull_request'
|
if: gitea.event_name == 'pull_request'
|
||||||
run: |
|
run: |
|
||||||
CURRENT=$(go tool cover -func=coverage.txt | grep "^total:" | awk '{print $NF}' | tr -d '%')
|
CURRENT="${{ steps.coverage.outputs.total-coverage }}"
|
||||||
if [ -f ./baseline/coverage.txt ]; then
|
if [ -f ./baseline/coverage.txt ]; then
|
||||||
BASE=$(go tool cover -func=./baseline/coverage.txt | grep "^total:" | awk '{print $NF}' | tr -d '%')
|
BASE=$(cat ./baseline/coverage.txt)
|
||||||
echo "Base coverage: ${BASE}%"
|
echo "Base coverage: ${BASE}%"
|
||||||
echo "Current coverage: ${CURRENT}%"
|
echo "Current coverage: ${CURRENT}%"
|
||||||
if [ "$(echo "$CURRENT < $BASE" | bc -l)" -eq 1 ]; then
|
if [ "$(echo "$CURRENT < $BASE" | bc -l)" -eq 1 ]; then
|
||||||
@@ -49,8 +46,9 @@ jobs:
|
|||||||
echo "No baseline coverage found, skipping comparison"
|
echo "No baseline coverage found, skipping comparison"
|
||||||
echo "Current coverage: ${CURRENT}%"
|
echo "Current coverage: ${CURRENT}%"
|
||||||
fi
|
fi
|
||||||
|
- name: Save coverage baseline
|
||||||
# Upload coverage as baseline (only on main)
|
if: gitea.ref == 'refs/heads/main'
|
||||||
|
run: echo "${{ steps.coverage.outputs.total-coverage }}" > coverage.txt
|
||||||
- name: Upload coverage baseline
|
- name: Upload coverage baseline
|
||||||
if: gitea.ref == 'refs/heads/main'
|
if: gitea.ref == 'refs/heads/main'
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
@@ -58,25 +56,23 @@ jobs:
|
|||||||
name: coverage-baseline
|
name: coverage-baseline
|
||||||
path: coverage.txt
|
path: coverage.txt
|
||||||
retention-days: 90
|
retention-days: 90
|
||||||
|
|
||||||
# Post coverage to PR comment
|
|
||||||
- name: Post coverage comment
|
- name: Post coverage comment
|
||||||
if: gitea.event_name == 'pull_request'
|
if: gitea.event_name == 'pull_request'
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
GITEA_URL: ${{ gitea.server_url }}
|
GITEA_URL: ${{ gitea.server_url }}
|
||||||
run: |
|
run: |
|
||||||
COVERAGE=$(go tool cover -func=coverage.txt | grep "^total:" | awk '{print $NF}')
|
COVERAGE="${{ steps.coverage.outputs.total-coverage }}"
|
||||||
curl -X POST "${GITEA_URL}/api/v1/repos/${{ gitea.repository }}/issues/${{ gitea.event.pull_request.number }}/comments" \
|
curl -X POST "${GITEA_URL}/api/v1/repos/${{ gitea.repository }}/issues/${{ gitea.event.pull_request.number }}/comments" \
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"body\": \"## Coverage Report\n\nTotal coverage: **${COVERAGE}**\"}"
|
-d "{\"body\": \"## Coverage Report\n\nTotal coverage: **${COVERAGE}%**\"}"
|
||||||
|
|
||||||
vulnerabilities:
|
vulnerabilities:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
go-version: 'stable'
|
||||||
- name: Check vulnerabilities
|
- name: Check vulnerabilities
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SKIP: no-commit-to-branch
|
SKIP: no-commit-to-branch
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: stable
|
go-version: stable
|
||||||
- uses: actions/setup-python@v6
|
- uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: '3.14'
|
python-version: '3.14'
|
||||||
- name: Install goimports
|
- name: Install goimports
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ repos:
|
|||||||
- --allow-multiple-documents
|
- --allow-multiple-documents
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
|
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
|
||||||
rev: v9.24.0
|
rev: v9.27.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: commitlint
|
- id: commitlint
|
||||||
stages: [ commit-msg ]
|
stages: [ commit-msg ]
|
||||||
@@ -30,10 +30,10 @@ repos:
|
|||||||
- id: go-test
|
- id: go-test
|
||||||
- id: gofumpt
|
- id: gofumpt
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
rev: v2.11.1
|
rev: v2.14.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: golangci-lint-full
|
- id: golangci-lint-full
|
||||||
- repo: https://github.com/gitleaks/gitleaks
|
- repo: https://github.com/gitleaks/gitleaks
|
||||||
rev: v8.30.0
|
rev: v8.30.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: gitleaks
|
- id: gitleaks
|
||||||
+106
@@ -2,6 +2,112 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.6.1] - 2026-09-19
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Close idle OTLP connections before the collector does (#184)
|
||||||
|
|
||||||
|
## [0.6.0] - 2026-09-17
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- *(metrics)* Record eventsourced pg outbox publishes and retries (#182)
|
||||||
|
|
||||||
|
## [0.5.0] - 2026-09-14
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- [**breaking**] Use eventsourced from Codeberg (#178)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.95 (#176)
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.13.2 (#174)
|
||||||
|
- Bump the minor version for breaking changes before 1.0.0 (#180)
|
||||||
|
|
||||||
|
## [0.4.2] - 2026-08-29
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update opentelemetry-go monorepo (#172)
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.13.0 (#168)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.13.1 (#170)
|
||||||
|
|
||||||
|
## [0.4.1] - 2026-08-07
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.92 (#154)
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.93 (#158)
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.94 (#160)
|
||||||
|
- *(deps)* Update opentelemetry-go monorepo (#166)
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update actions/checkout action to v7 (#152)
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.26.0 (#156)
|
||||||
|
- *(deps)* Update actions/setup-go action to v7 (#162)
|
||||||
|
- *(deps)* Update actions/setup-python action to v7 (#164)
|
||||||
|
|
||||||
|
## [0.4.0] - 2026-06-16
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- *(metrics)* Add SubscriptionMetrics OTel observer for subscriptions (#150)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update opentelemetry-go monorepo (#146)
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.91 (#148)
|
||||||
|
|
||||||
|
## [0.3.1] - 2026-05-29
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Set service.instance.id for unique instance label (#144)
|
||||||
|
|
||||||
|
## [0.3.0] - 2026-05-26
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Add eventsourced MetricsRecorder adapter for OpenTelemetry (#142)
|
||||||
|
|
||||||
|
## [0.2.7] - 2026-05-09
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update opentelemetry-go monorepo (#131)
|
||||||
|
- *(ci)* Use go-test-coverage binary directly to fix Gitea Actions (#134)
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.90 (#133)
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.25.0 (#136)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.12.0 (#137)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.12.1 (#138)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.12.2 (#140)
|
||||||
|
|
||||||
|
## [0.2.6] - 2026-04-02
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.88 (#121)
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.89 (#129)
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.11.2 (#119)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.11.3 (#123)
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.30.1 (#125)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.11.4 (#127)
|
||||||
|
|
||||||
## [0.2.5] - 2026-03-06
|
## [0.2.5] - 2026-03-06
|
||||||
|
|
||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|||||||
@@ -42,3 +42,4 @@ http.Handle("/", otelsetup.Handler(myHandler))
|
|||||||
|
|
||||||
- `OTEL_RESOURCE_ATTRIBUTES` - Auto-set if not provided (service.name, service.version, service.environment)
|
- `OTEL_RESOURCE_ATTRIBUTES` - Auto-set if not provided (service.name, service.version, service.environment)
|
||||||
- Standard OTLP environment variables for endpoint configuration
|
- Standard OTLP environment variables for endpoint configuration
|
||||||
|
- The trace and metric exporters use our own HTTP client (idle connections closed after 30s, before the collector's 1m idle timeout), so `OTEL_EXPORTER_OTLP_*TIMEOUT` and the `OTEL_EXPORTER_OTLP_*CERTIFICATE` / `*CLIENT_*` TLS variables are ignored
|
||||||
@@ -78,3 +78,7 @@ filter_commits = false
|
|||||||
topo_order = false
|
topo_order = false
|
||||||
# sort the commits inside sections by oldest/newest order
|
# sort the commits inside sections by oldest/newest order
|
||||||
sort_commits = "oldest"
|
sort_commits = "oldest"
|
||||||
|
|
||||||
|
[bump]
|
||||||
|
# Before 1.0.0, a breaking change bumps the minor version instead of the major.
|
||||||
|
breaking_always_bump_major = false
|
||||||
@@ -1,39 +1,47 @@
|
|||||||
module gitea.unbound.se/shiny/otelsetup
|
module gitea.unbound.se/shiny/otelsetup
|
||||||
|
|
||||||
go 1.25.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/gqlgen v0.17.87
|
codeberg.org/eventsourced/eventsourced v1.24.0
|
||||||
go.opentelemetry.io/otel v1.42.0
|
codeberg.org/eventsourced/pg/v2 v2.3.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0
|
github.com/99designs/gqlgen v0.17.95
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0
|
go.opentelemetry.io/otel v1.47.0
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0
|
||||||
go.opentelemetry.io/otel/log v0.18.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0
|
||||||
go.opentelemetry.io/otel/sdk v1.42.0
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.23.0
|
||||||
go.opentelemetry.io/otel/sdk/log v0.18.0
|
go.opentelemetry.io/otel/log v1.47.0
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.42.0
|
go.opentelemetry.io/otel/metric v1.47.0
|
||||||
go.opentelemetry.io/otel/trace v1.42.0
|
go.opentelemetry.io/otel/sdk v1.47.0
|
||||||
|
go.opentelemetry.io/otel/sdk/log v1.47.0
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.47.0
|
||||||
|
go.opentelemetry.io/otel/trace v1.47.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.4 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect
|
||||||
github.com/sosodev/duration v1.3.1 // indirect
|
github.com/jmoiron/sqlx v1.4.0 // indirect
|
||||||
github.com/vektah/gqlparser/v2 v2.5.32 // indirect
|
github.com/lib/pq v1.12.3 // indirect
|
||||||
|
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||||
|
github.com/pressly/goose/v3 v3.28.0 // indirect
|
||||||
|
github.com/sethvargo/go-retry v0.4.0 // indirect
|
||||||
|
github.com/sosodev/duration v1.4.0 // indirect
|
||||||
|
github.com/vektah/gqlparser/v2 v2.5.37 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.42.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/net v0.51.0 // indirect
|
golang.org/x/net v0.59.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.23.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.48.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.42.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect
|
||||||
google.golang.org/grpc v1.79.2 // indirect
|
google.golang.org/grpc v1.83.2 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.12 // indirect
|
||||||
)
|
)
|
||||||
@@ -1,85 +1,124 @@
|
|||||||
github.com/99designs/gqlgen v0.17.87 h1:pSnCIMhBQezAE8bc1GNmfdLXFmnWtWl1GRDFEE/nHP8=
|
codeberg.org/eventsourced/eventsourced v1.24.0 h1:yNB+74JNB/NHo7D8ZR/yQcTVDYrWnjFf0xUKLgViqRc=
|
||||||
github.com/99designs/gqlgen v0.17.87/go.mod h1:fK05f1RqSNfQpd4CfW5qk/810Tqi4/56Wf6Nem0khAg=
|
codeberg.org/eventsourced/eventsourced v1.24.0/go.mod h1:uy0HxejDdNFpI+Yp7VOEvjfEUFJQX1VkCZ4e4JyDp7Q=
|
||||||
|
codeberg.org/eventsourced/pg/v2 v2.3.0 h1:g6kZ9DASeFrC8C+bDcXzbYD8409KHzWmyLOI5sCdNWI=
|
||||||
|
codeberg.org/eventsourced/pg/v2 v2.3.0/go.mod h1:qPxl3382m7lbhbrEaxwSZ9PrAPoODj2+TqnV3tIKWzU=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
|
github.com/99designs/gqlgen v0.17.95 h1:882h7F5iJImgtyUVttc4MOK2NbzbMYc2oyNeHqkjpP4=
|
||||||
|
github.com/99designs/gqlgen v0.17.95/go.mod h1:kHYPrpwOXDU1OQyxIg3Z7nVXSnlUoHVWBY7CMJCAM4M=
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
||||||
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||||
|
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||||
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||||
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
|
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||||
|
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/pressly/goose/v3 v3.28.0 h1:D2M+iL31GmpZxSHOhX8mqyqAT3CXnokUmm0eKoSP+Vc=
|
||||||
|
github.com/pressly/goose/v3 v3.28.0/go.mod h1:v26MOuB8bL3kzzrt3Vqhb3R0PRVsl8hFQKdrht/L6Rk=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
|
||||||
|
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
|
||||||
|
github.com/sethvargo/go-retry v0.4.0 h1:9qy1OoIAxBL+gBYnkTnTnWle5wlfsXQlwRzIbbpdqPw=
|
||||||
|
github.com/sethvargo/go-retry v0.4.0/go.mod h1:tvsjdKG6xfiCx4LSiUZ06kcv38xvdVQwv8R6/VnnVWg=
|
||||||
|
github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE=
|
||||||
|
github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
||||||
|
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||||
|
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||||
|
github.com/vektah/gqlparser/v2 v2.5.37 h1:jbb1Ilv+xBklV6653tKb4oVUupPNTLb5LmrnBKVI12Y=
|
||||||
|
github.com/vektah/gqlparser/v2 v2.5.37/go.mod h1:9O4Ox6Ngd3Y12bMD3w6i3CRQXh8W1oC1q0m6olCymDM=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM=
|
||||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 h1:7kszRN4Ze5GeAkUnBjBSUIunVBMA+mZIG04auueqguM=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0/go.mod h1:CLzakYy8jOdkBfcveCWYkeKJxujcF4xPhh2Z7IfE98U=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 h1:KJVjPD3rcPb98rIs3HznyJlrfx9ge5oJvxxlGR+P/7s=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.23.0 h1:PW/E9Tw6ZykC88pey0MZY8hKOUeEw+hX6PU+oXA4A9o=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0/go.mod h1:K3kRa2ckmHWQaTWQdPRHc7qGXASuVuoEQXzrvlA98Ws=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.23.0/go.mod h1:hUw4VNfiOnQ/yzGgy6ZdWVnudXw7iRfImhd6FjXOnvI=
|
||||||
go.opentelemetry.io/otel/log v0.18.0 h1:XgeQIIBjZZrliksMEbcwMZefoOSMI1hdjiLEiiB0bAg=
|
go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI=
|
||||||
go.opentelemetry.io/otel/log v0.18.0/go.mod h1:KEV1kad0NofR3ycsiDH4Yjcoj0+8206I6Ox2QYFSNgI=
|
go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw=
|
||||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY=
|
||||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU=
|
||||||
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDRbe9deYpM=
|
||||||
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.18.0 h1:n8OyZr7t7otkeTnPTbDNom6rW16TBYGtvyy2Gk6buQw=
|
go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.18.0/go.mod h1:C0+wxkTwKpOCZLrlJ3pewPiiQwpzycPI/u6W0Z9fuYk=
|
go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.18.0 h1:l3mYuPsuBx6UKE47BVcPrZoZ0q/KER57vbj2qkgDLXA=
|
go.opentelemetry.io/otel/sdk/log v1.47.0 h1:W4N/ZfgpkRRnmARnqYoQ6BrWA0mrj8IzVDNLlpmUJZY=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.18.0/go.mod h1:7cHtiVJpZebB3wybTa4NG+FUo5NPe3PROz1FqB0+qdw=
|
go.opentelemetry.io/otel/sdk/log v1.47.0/go.mod h1:Wu74Z2EmOKXi2FleSNAb0Hyau2G+r5kubSEMJw5aAMs=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.23.0 h1:j3pm42QCrKYYLLkI6OrV1QHFkUfDA6/4EP+Y1PzggLg=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.23.0/go.mod h1:zt0gEnuTZgDvqvczHwYswz93eqMaIr6upTTsdV/HUlU=
|
||||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw=
|
||||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw=
|
||||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM=
|
||||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
|
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
|
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE=
|
||||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
|
||||||
|
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||||
|
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
modernc.org/libc v1.75.6 h1:yKk8qo+Di4gkmvRboK8ocCqH22FiUCR6jRy2OwtCRus=
|
||||||
|
modernc.org/libc v1.75.6/go.mod h1:bO5o2ztHxBb2rjz0PgdHN0sSMw57CgxGFLZ3Qd/QpVQ=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
|
||||||
|
modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/sqlite v1.57.0 h1:qNQP6xnx5M0ISNtlnxoOX0+cD5bJ0/gr9aMmndFczzg=
|
||||||
|
modernc.org/sqlite v1.57.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
|
||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
package otelsetup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"codeberg.org/eventsourced/eventsourced"
|
||||||
|
"codeberg.org/eventsourced/pg/v2"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
)
|
||||||
|
|
||||||
|
// eventsourcedMeterName is the instrumentation scope for the event-sourcing
|
||||||
|
// metrics emitted by the adapter returned from NewEventsourcedMetrics.
|
||||||
|
const eventsourcedMeterName = "gitea.unbound.se/shiny/otelsetup/eventsourced"
|
||||||
|
|
||||||
|
// durationBucketsSeconds are explicit histogram boundaries tuned for
|
||||||
|
// sub-second event-store and command latencies. The SDK default boundaries are
|
||||||
|
// scaled for milliseconds, which would bucket nearly every second-valued
|
||||||
|
// observation into the first bucket and make percentiles useless.
|
||||||
|
var durationBucketsSeconds = []float64{0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10}
|
||||||
|
|
||||||
|
// eventsourcedMetrics implements eventsourced.MetricsRecorder by translating
|
||||||
|
// the framework's Metric values into OpenTelemetry instruments registered on
|
||||||
|
// the global MeterProvider configured by SetupOTelSDK.
|
||||||
|
//
|
||||||
|
// The OTel metric instruments are safe for concurrent use, and the struct is
|
||||||
|
// immutable after construction, so Record may be called from multiple
|
||||||
|
// goroutines as the framework requires.
|
||||||
|
//
|
||||||
|
// Operation counts are read off each duration histogram's generated _count
|
||||||
|
// series rather than separate counters; the only standalone counters carry
|
||||||
|
// information a histogram count cannot (events.loaded sums the number of events
|
||||||
|
// per load, idempotency.checks counts lookups that have no duration).
|
||||||
|
type eventsourcedMetrics struct {
|
||||||
|
commandDuration metric.Float64Histogram
|
||||||
|
eventStoreDur metric.Float64Histogram
|
||||||
|
eventsLoaded metric.Int64Counter
|
||||||
|
eventLoadDur metric.Float64Histogram
|
||||||
|
snapshotStoreDur metric.Float64Histogram
|
||||||
|
snapshotLoadDur metric.Float64Histogram
|
||||||
|
idempotencyCheck metric.Int64Counter
|
||||||
|
outboxPublishDur metric.Float64Histogram
|
||||||
|
outboxRetries metric.Int64Counter
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEventsourcedMetrics builds an eventsourced.MetricsRecorder that records to
|
||||||
|
// the global OpenTelemetry MeterProvider. Pass the result to both
|
||||||
|
// pg.WithMetrics (for event-store operations) and eventsourced.WithMetrics
|
||||||
|
// (for command handling) so a single recorder covers store and handler
|
||||||
|
// metrics.
|
||||||
|
//
|
||||||
|
// SetupOTelSDK must have run first so the global MeterProvider is configured;
|
||||||
|
// when metrics are disabled the global provider is a no-op and recording is
|
||||||
|
// effectively free.
|
||||||
|
func NewEventsourcedMetrics() (eventsourced.MetricsRecorder, error) {
|
||||||
|
m := otel.Meter(eventsourcedMeterName)
|
||||||
|
var errs []error
|
||||||
|
hist := func(name, desc string) metric.Float64Histogram {
|
||||||
|
h, err := m.Float64Histogram(
|
||||||
|
name,
|
||||||
|
metric.WithDescription(desc),
|
||||||
|
metric.WithUnit("s"),
|
||||||
|
metric.WithExplicitBucketBoundaries(durationBucketsSeconds...),
|
||||||
|
)
|
||||||
|
errs = append(errs, err)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
counter := func(name, desc string) metric.Int64Counter {
|
||||||
|
c, err := m.Int64Counter(name, metric.WithDescription(desc))
|
||||||
|
errs = append(errs, err)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &eventsourcedMetrics{
|
||||||
|
commandDuration: hist("eventsourced.command.duration", "Wall-clock time to process a command in Handle."),
|
||||||
|
eventStoreDur: hist("eventsourced.event.store.duration", "Time taken to persist a single event."),
|
||||||
|
eventsLoaded: counter("eventsourced.events.loaded", "Number of events loaded when rehydrating aggregates."),
|
||||||
|
eventLoadDur: hist("eventsourced.event.load.duration", "Time taken to load events for an aggregate."),
|
||||||
|
snapshotStoreDur: hist("eventsourced.snapshot.store.duration", "Time taken to persist a snapshot."),
|
||||||
|
snapshotLoadDur: hist("eventsourced.snapshot.load.duration", "Time taken to load a snapshot."),
|
||||||
|
idempotencyCheck: counter("eventsourced.idempotency.checks", "Number of command idempotency lookups."),
|
||||||
|
outboxPublishDur: hist("eventsourced.outbox.publish.duration", "Time taken to publish an event from the outbox."),
|
||||||
|
outboxRetries: counter("eventsourced.outbox.retries", "Failed outbox publishes; permanent ones are never retried."),
|
||||||
|
}
|
||||||
|
if err := errors.Join(errs...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record implements eventsourced.MetricsRecorder. Metric types the adapter does
|
||||||
|
// not recognise are ignored, and so are pg.OutboxBatch and pg.OutboxCleanup: a failed
|
||||||
|
// publish already shows in the publish and retry instruments.
|
||||||
|
func (e *eventsourcedMetrics) Record(ctx context.Context, raw eventsourced.Metric) {
|
||||||
|
switch m := raw.(type) {
|
||||||
|
case eventsourced.CommandDuration:
|
||||||
|
e.commandDuration.Record(ctx, m.Duration.Seconds(), metric.WithAttributes(
|
||||||
|
attribute.String("command.type", m.CommandType),
|
||||||
|
attribute.Bool("success", m.Success),
|
||||||
|
))
|
||||||
|
case eventsourced.EventStored:
|
||||||
|
e.eventStoreDur.Record(ctx, m.Duration.Seconds(), metric.WithAttributes(
|
||||||
|
attribute.String("aggregate.type", m.AggregateType),
|
||||||
|
attribute.String("event.type", m.EventType),
|
||||||
|
))
|
||||||
|
case eventsourced.EventsLoaded:
|
||||||
|
attrs := metric.WithAttributes(attribute.String("aggregate.type", m.AggregateType))
|
||||||
|
e.eventsLoaded.Add(ctx, int64(m.EventCount), attrs)
|
||||||
|
e.eventLoadDur.Record(ctx, m.Duration.Seconds(), attrs)
|
||||||
|
case eventsourced.SnapshotStored:
|
||||||
|
e.snapshotStoreDur.Record(ctx, m.Duration.Seconds(), metric.WithAttributes(
|
||||||
|
attribute.String("aggregate.type", m.AggregateType),
|
||||||
|
attribute.Bool("success", m.Success),
|
||||||
|
))
|
||||||
|
case eventsourced.SnapshotLoaded:
|
||||||
|
e.snapshotLoadDur.Record(ctx, m.Duration.Seconds(), metric.WithAttributes(
|
||||||
|
attribute.String("aggregate.type", m.AggregateType),
|
||||||
|
attribute.Bool("found", m.Found),
|
||||||
|
))
|
||||||
|
case eventsourced.IdempotencyCheck:
|
||||||
|
e.idempotencyCheck.Add(ctx, 1, metric.WithAttributes(
|
||||||
|
attribute.String("aggregate.type", m.AggregateType),
|
||||||
|
attribute.Bool("hit", m.Hit),
|
||||||
|
))
|
||||||
|
case pg.OutboxPublish:
|
||||||
|
e.outboxPublishDur.Record(ctx, m.Duration.Seconds(), metric.WithAttributes(
|
||||||
|
attribute.String("event.type", m.EventType),
|
||||||
|
attribute.Bool("success", m.Success),
|
||||||
|
))
|
||||||
|
case pg.OutboxRetry:
|
||||||
|
e.outboxRetries.Add(ctx, 1, metric.WithAttributes(
|
||||||
|
attribute.String("event.type", m.EventType),
|
||||||
|
attribute.Bool("permanent", m.Permanent),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package otelsetup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeberg.org/eventsourced/eventsourced"
|
||||||
|
"codeberg.org/eventsourced/pg/v2"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewEventsourcedMetrics_RecordsContract(t *testing.T) {
|
||||||
|
reader := sdkmetric.NewManualReader()
|
||||||
|
otel.SetMeterProvider(sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)))
|
||||||
|
|
||||||
|
r, err := NewEventsourcedMetrics()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewEventsourcedMetrics returned error: %v", err)
|
||||||
|
}
|
||||||
|
if r == nil {
|
||||||
|
t.Fatal("NewEventsourcedMetrics returned nil recorder")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recording every known metric type (and an unknown one) must not panic
|
||||||
|
// and must emit the expected instruments.
|
||||||
|
for _, m := range []eventsourced.Metric{
|
||||||
|
eventsourced.CommandDuration{CommandType: "AddEntry", Duration: time.Millisecond, Success: true},
|
||||||
|
eventsourced.EventStored{AggregateType: "Entry", EventType: "EntryAdded", Duration: time.Millisecond},
|
||||||
|
eventsourced.EventsLoaded{AggregateType: "Entry", EventCount: 3, Duration: time.Millisecond},
|
||||||
|
eventsourced.SnapshotStored{AggregateType: "Entry", Duration: time.Millisecond, Success: true},
|
||||||
|
eventsourced.SnapshotLoaded{AggregateType: "Entry", Found: false, Duration: time.Millisecond},
|
||||||
|
eventsourced.IdempotencyCheck{AggregateType: "Entry", Hit: true},
|
||||||
|
pg.OutboxPublish{EventType: "EntryAdded", Success: false, Duration: time.Millisecond},
|
||||||
|
pg.OutboxRetry{EventType: "EntryAdded", RetryCount: 1},
|
||||||
|
unknownMetric{},
|
||||||
|
} {
|
||||||
|
r.Record(context.Background(), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rm metricdata.ResourceMetrics
|
||||||
|
if err := reader.Collect(context.Background(), &rm); err != nil {
|
||||||
|
t.Fatalf("collect: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := map[string]bool{}
|
||||||
|
for _, sm := range rm.ScopeMetrics {
|
||||||
|
for _, md := range sm.Metrics {
|
||||||
|
got[md.Name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
want := []string{
|
||||||
|
"eventsourced.command.duration",
|
||||||
|
"eventsourced.event.store.duration",
|
||||||
|
"eventsourced.events.loaded",
|
||||||
|
"eventsourced.event.load.duration",
|
||||||
|
"eventsourced.snapshot.store.duration",
|
||||||
|
"eventsourced.snapshot.load.duration",
|
||||||
|
"eventsourced.idempotency.checks",
|
||||||
|
"eventsourced.outbox.publish.duration",
|
||||||
|
"eventsourced.outbox.retries",
|
||||||
|
}
|
||||||
|
var missing []string
|
||||||
|
for _, w := range want {
|
||||||
|
if !got[w] {
|
||||||
|
missing = append(missing, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(missing) > 0 {
|
||||||
|
sort.Strings(missing)
|
||||||
|
t.Errorf("missing expected metrics: %v", missing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type unknownMetric struct{}
|
||||||
|
|
||||||
|
func (unknownMetric) IsMetric() {}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
@@ -22,7 +23,17 @@ import (
|
|||||||
// SetupOTelSDK bootstraps the OpenTelemetry pipeline.
|
// SetupOTelSDK bootstraps the OpenTelemetry pipeline.
|
||||||
func SetupOTelSDK(ctx context.Context, enabled bool, serviceName, buildVersion, environment string) (func(context.Context) error, error) {
|
func SetupOTelSDK(ctx context.Context, enabled bool, serviceName, buildVersion, environment string) (func(context.Context) error, error) {
|
||||||
if os.Getenv("OTEL_RESOURCE_ATTRIBUTES") == "" {
|
if os.Getenv("OTEL_RESOURCE_ATTRIBUTES") == "" {
|
||||||
if err := os.Setenv("OTEL_RESOURCE_ATTRIBUTES", fmt.Sprintf("service.name=%s,service.version=%s,service.environment=%s", serviceName, buildVersion, environment)); err != nil {
|
// service.instance.id makes every pod a distinct telemetry resource. The
|
||||||
|
// OTLP→Prometheus exporter maps it to the `instance` label on metrics and
|
||||||
|
// target_info, which keeps multi-replica services from colliding on a
|
||||||
|
// single series and gives joins a unique (job, instance) key. Hostname is
|
||||||
|
// the pod name under Kubernetes; fall back to the service name if it is
|
||||||
|
// unavailable so the attribute is always present.
|
||||||
|
instanceID, err := os.Hostname()
|
||||||
|
if err != nil || instanceID == "" {
|
||||||
|
instanceID = serviceName
|
||||||
|
}
|
||||||
|
if err := os.Setenv("OTEL_RESOURCE_ATTRIBUTES", fmt.Sprintf("service.name=%s,service.version=%s,service.environment=%s,service.instance.id=%s", serviceName, buildVersion, environment, instanceID)); err != nil {
|
||||||
return func(context.Context) error {
|
return func(context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}, err
|
}, err
|
||||||
@@ -55,7 +66,7 @@ func SetupOTelSDK(ctx context.Context, enabled bool, serviceName, buildVersion,
|
|||||||
)
|
)
|
||||||
otel.SetTextMapPropagator(prop)
|
otel.SetTextMapPropagator(prop)
|
||||||
|
|
||||||
traceExporter, err := otlptracehttp.New(ctx)
|
traceExporter, err := otlptracehttp.New(ctx, otlptracehttp.WithHTTPClient(otlpHTTPClient()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return handleErr(err)
|
return handleErr(err)
|
||||||
}
|
}
|
||||||
@@ -78,7 +89,7 @@ func SetupOTelSDK(ctx context.Context, enabled bool, serviceName, buildVersion,
|
|||||||
global.SetLoggerProvider(logProvider)
|
global.SetLoggerProvider(logProvider)
|
||||||
shutdownFuncs = append(shutdownFuncs, logProvider.Shutdown)
|
shutdownFuncs = append(shutdownFuncs, logProvider.Shutdown)
|
||||||
|
|
||||||
exp, err := otlpmetrichttp.New(ctx)
|
exp, err := otlpmetrichttp.New(ctx, otlpmetrichttp.WithHTTPClient(otlpHTTPClient()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return handleErr(err)
|
return handleErr(err)
|
||||||
}
|
}
|
||||||
@@ -89,6 +100,36 @@ func SetupOTelSDK(ctx context.Context, enabled bool, serviceName, buildVersion,
|
|||||||
return shutdown, err
|
return shutdown, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// otlpHTTPClient is the OTLP exporters' default client, except that it drops idle
|
||||||
|
// connections after 30s. The collector (Alloy's OTLP receiver, idle_timeout 1m) closes
|
||||||
|
// connections idle for a minute, and metrics are pushed every 60s. With the exporters'
|
||||||
|
// default of 90s, a push could reuse a connection the collector was closing. The push
|
||||||
|
// then failed (EOF, connection reset) and was dropped: net/http doesn't retry a POST and
|
||||||
|
// the exporters don't retry transport errors. Traces hit the same race after a quiet
|
||||||
|
// spell. Closing first means such a push dials fresh.
|
||||||
|
//
|
||||||
|
// The transport mirrors the exporters' own (otlpmetrichttp/otlptracehttp v1.46.0
|
||||||
|
// ourTransport). A custom client makes the exporters ignore OTEL_EXPORTER_OTLP_*TIMEOUT
|
||||||
|
// and the OTEL_EXPORTER_OTLP_*CERTIFICATE/CLIENT_* TLS variables; nothing sets them.
|
||||||
|
func otlpHTTPClient() *http.Client {
|
||||||
|
return &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
IdleConnTimeout: 30 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
},
|
||||||
|
// The exporters' default.
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Handler(h http.Handler) http.Handler {
|
func Handler(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
|
ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package otelsetup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOTLPHTTPClient_ClosesIdleConnectionsBeforeTheCollector(t *testing.T) {
|
||||||
|
c := otlpHTTPClient()
|
||||||
|
tr, ok := c.Transport.(*http.Transport)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("transport is %T, want *http.Transport", c.Transport)
|
||||||
|
}
|
||||||
|
// Alloy's OTLP receiver closes connections idle for 1m; the client must close first.
|
||||||
|
if tr.IdleConnTimeout <= 0 || tr.IdleConnTimeout >= time.Minute {
|
||||||
|
t.Errorf("IdleConnTimeout = %v, want in (0, 1m)", tr.IdleConnTimeout)
|
||||||
|
}
|
||||||
|
if c.Timeout != 10*time.Second {
|
||||||
|
t.Errorf("Timeout = %v, want 10s (the exporters' default)", c.Timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,5 +2,15 @@
|
|||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": [
|
||||||
"config:recommended"
|
"config:recommended"
|
||||||
|
],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"groupName": "OpenTelemetry",
|
||||||
|
"matchPackageNames": [
|
||||||
|
"go.opentelemetry.io/**",
|
||||||
|
"gitea.unbound.se/shiny/otelsetup",
|
||||||
|
"gitea.unbound.se/shiny/logging"
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package otelsetup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
)
|
||||||
|
|
||||||
|
// subscriptionMeterName is the instrumentation scope for the cross-service
|
||||||
|
// subscription push metrics emitted by SubscriptionMetrics.
|
||||||
|
const subscriptionMeterName = "gitea.unbound.se/shiny/otelsetup/subscriptions"
|
||||||
|
|
||||||
|
// SubscriptionMetrics records cross-service subscription push outcomes (the
|
||||||
|
// read-your-writes subscriptions described in ADR-0012) as OpenTelemetry
|
||||||
|
// counters.
|
||||||
|
//
|
||||||
|
// Its method set (Pushed/PushSkipped/Dropped/ChannelFull) satisfies the
|
||||||
|
// Observer interface of gitea.unbound.se/shiny/subscriptions *structurally* —
|
||||||
|
// otelsetup does not import that library. A service constructs it and passes it
|
||||||
|
// to the registry:
|
||||||
|
//
|
||||||
|
// metrics, err := otelsetup.NewSubscriptionMetrics("availableCompanies")
|
||||||
|
// if err != nil { return err }
|
||||||
|
// reg := subscriptions.New[T](subscriptions.WithObserver(metrics))
|
||||||
|
//
|
||||||
|
// The WithObserver call type-checks the structural match, so a drift in the
|
||||||
|
// Observer interface fails the service build. Add a
|
||||||
|
// `var _ subscriptions.Observer = (*otelsetup.SubscriptionMetrics)(nil)` next to
|
||||||
|
// it for an explicit guard.
|
||||||
|
//
|
||||||
|
// Outcomes are recorded against a low-cardinality (subscription, outcome) pair.
|
||||||
|
// The subscriber key (company id / user email) the Observer methods receive is
|
||||||
|
// deliberately NOT used as a metric attribute — that would be unbounded
|
||||||
|
// cardinality; the key stays in the subscriptions library's logs for
|
||||||
|
// correlation.
|
||||||
|
type SubscriptionMetrics struct {
|
||||||
|
notifications metric.Int64Counter
|
||||||
|
subscription attribute.KeyValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSubscriptionMetrics builds a SubscriptionMetrics that records to the global
|
||||||
|
// OpenTelemetry MeterProvider. subscription is the low-cardinality name of the
|
||||||
|
// subscription field (e.g. "availableCompanies", "entryBasesChanged"), recorded
|
||||||
|
// as an attribute so one counter covers every subscription.
|
||||||
|
//
|
||||||
|
// SetupOTelSDK must have run first so the global MeterProvider is configured;
|
||||||
|
// when metrics are disabled the global provider is a no-op and recording is
|
||||||
|
// effectively free.
|
||||||
|
func NewSubscriptionMetrics(subscription string) (*SubscriptionMetrics, error) {
|
||||||
|
c, err := otel.Meter(subscriptionMeterName).Int64Counter(
|
||||||
|
"subscription.notifications",
|
||||||
|
metric.WithDescription("Cross-service subscription push outcomes, by outcome (pushed/skipped/dropped/channel_full)."),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &SubscriptionMetrics{
|
||||||
|
notifications: c,
|
||||||
|
subscription: attribute.String("subscription", subscription),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubscriptionMetrics) record(outcome string) {
|
||||||
|
s.notifications.Add(context.Background(), 1, metric.WithAttributes(
|
||||||
|
s.subscription,
|
||||||
|
attribute.String("outcome", outcome),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pushed records a change that was gated and delivered to the key's subscribers
|
||||||
|
// — the denominator for a skip/drop rate.
|
||||||
|
func (s *SubscriptionMetrics) Pushed(string) { s.record("pushed") }
|
||||||
|
|
||||||
|
// PushSkipped records a push skipped because the read view never reflected the
|
||||||
|
// change within the retry budget.
|
||||||
|
func (s *SubscriptionMetrics) PushSkipped(string) { s.record("skipped") }
|
||||||
|
|
||||||
|
// Dropped records a notification dropped because the worker queue was full.
|
||||||
|
func (s *SubscriptionMetrics) Dropped(string) { s.record("dropped") }
|
||||||
|
|
||||||
|
// ChannelFull records a notification dropped because a subscriber's buffer was
|
||||||
|
// full.
|
||||||
|
func (s *SubscriptionMetrics) ChannelFull(string) { s.record("channel_full") }
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package otelsetup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric/noop"
|
||||||
|
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
||||||
|
)
|
||||||
|
|
||||||
|
// observerShape mirrors gitea.unbound.se/shiny/subscriptions.Observer. This
|
||||||
|
// compile-time assertion guards that SubscriptionMetrics still satisfies that
|
||||||
|
// interface structurally, without otelsetup importing the library.
|
||||||
|
//
|
||||||
|
// KEEP IN SYNC with subscriptions.Observer: this only proves SubscriptionMetrics
|
||||||
|
// matches this local copy. The authoritative check that the local copy still
|
||||||
|
// matches the real interface is the `subscriptions.WithObserver(...)` call site
|
||||||
|
// in each consuming service — keep a `var _ subscriptions.Observer` guard there.
|
||||||
|
type observerShape interface {
|
||||||
|
Pushed(string)
|
||||||
|
PushSkipped(string)
|
||||||
|
Dropped(string)
|
||||||
|
ChannelFull(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ observerShape = (*SubscriptionMetrics)(nil)
|
||||||
|
|
||||||
|
// TestSubscriptionMetrics_DisabledProviderIsSafe proves the "recording is free
|
||||||
|
// when metrics are disabled" claim: with the default global no-op provider, the
|
||||||
|
// methods neither panic nor emit instruments.
|
||||||
|
func TestSubscriptionMetrics_DisabledProviderIsSafe(t *testing.T) {
|
||||||
|
otel.SetMeterProvider(noop.NewMeterProvider())
|
||||||
|
|
||||||
|
m, err := NewSubscriptionMetrics("entryBasesChanged")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewSubscriptionMetrics returned error: %v", err)
|
||||||
|
}
|
||||||
|
// Must not panic on the no-op provider.
|
||||||
|
m.Pushed("c1")
|
||||||
|
m.PushSkipped("c1")
|
||||||
|
m.Dropped("c1")
|
||||||
|
m.ChannelFull("c1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSubscriptionMetrics_RecordsOutcomes(t *testing.T) {
|
||||||
|
reader := sdkmetric.NewManualReader()
|
||||||
|
otel.SetMeterProvider(sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)))
|
||||||
|
|
||||||
|
m, err := NewSubscriptionMetrics("availableCompanies")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewSubscriptionMetrics returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The subscriber key is ignored for metrics; different keys must not create
|
||||||
|
// new series (cardinality guard is implicit — we only label by outcome).
|
||||||
|
m.Pushed("c1")
|
||||||
|
m.Pushed("c2")
|
||||||
|
m.PushSkipped("c1")
|
||||||
|
m.Dropped("c1")
|
||||||
|
m.ChannelFull("c1")
|
||||||
|
|
||||||
|
var rm metricdata.ResourceMetrics
|
||||||
|
if err := reader.Collect(context.Background(), &rm); err != nil {
|
||||||
|
t.Fatalf("collect: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
counts := map[string]int64{}
|
||||||
|
dataPoints := 0
|
||||||
|
found := false
|
||||||
|
for _, sm := range rm.ScopeMetrics {
|
||||||
|
for _, md := range sm.Metrics {
|
||||||
|
if md.Name != "subscription.notifications" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
sum, ok := md.Data.(metricdata.Sum[int64])
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected Sum[int64], got %T", md.Data)
|
||||||
|
}
|
||||||
|
for _, dp := range sum.DataPoints {
|
||||||
|
dataPoints++
|
||||||
|
sub, _ := dp.Attributes.Value(attribute.Key("subscription"))
|
||||||
|
if sub.AsString() != "availableCompanies" {
|
||||||
|
t.Errorf("unexpected subscription attribute: %q", sub.AsString())
|
||||||
|
}
|
||||||
|
outcome, _ := dp.Attributes.Value(attribute.Key("outcome"))
|
||||||
|
counts[outcome.AsString()] += dp.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Fatal("subscription.notifications counter not emitted")
|
||||||
|
}
|
||||||
|
// One series per outcome, keyed by outcome only (not by subscriber key).
|
||||||
|
if dataPoints != 4 {
|
||||||
|
t.Errorf("expected 4 data points (one per outcome), got %d", dataPoints)
|
||||||
|
}
|
||||||
|
want := map[string]int64{"pushed": 2, "skipped": 1, "dropped": 1, "channel_full": 1}
|
||||||
|
for k, v := range want {
|
||||||
|
if counts[k] != v {
|
||||||
|
t.Errorf("outcome %q: got %d, want %d", k, counts[k], v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user