mirror of
https://github.com/oxynote/oxynote.git
synced 2026-10-17 10:59:20 +02:00
add built-in demo prometheus to core
This commit is contained in:
parent
bd0500c574
commit
996fba144b
74
.github/workflows/qa-datagen.yml
vendored
Normal file
74
.github/workflows/qa-datagen.yml
vendored
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
name: QA
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- datagen/**
|
||||
- .github/workflows/qa-datagen.yml
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- datagen/**
|
||||
- .github/workflows/qa-datagen.yml
|
||||
|
||||
# a newer push supersedes the still-running workflow of the stale commit
|
||||
# and cancels it. The group carries a hardcoded per-file prefix because
|
||||
# every QA workflow shares the display name "QA", so github.workflow
|
||||
# would put them all in one group and let a run of one workflow cancel
|
||||
# an in-flight run of another.
|
||||
concurrency:
|
||||
group: qa-datagen-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Linting datagen
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: datagen/go.mod
|
||||
cache-dependency-path: datagen/go.sum
|
||||
|
||||
- name: Installing dependencies
|
||||
run: go mod download
|
||||
working-directory: datagen
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.13
|
||||
working-directory: datagen
|
||||
|
||||
test:
|
||||
name: Unit testing datagen
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: datagen/go.mod
|
||||
cache-dependency-path: datagen/go.sum
|
||||
|
||||
- name: Installing dependencies
|
||||
run: go mod download
|
||||
working-directory: datagen
|
||||
|
||||
# the pgdemo and mariademo suites run against throwaway containers,
|
||||
# which the ubuntu runners provide a docker daemon for out of the box.
|
||||
- name: Run unit tests
|
||||
run: make check-coverage
|
||||
working-directory: datagen
|
||||
|
|
@ -77,7 +77,7 @@ Strong success criteria let you loop independently. Weak criteria ("make it work
|
|||
- `datagen/` — demo-data generator; separate Go module `github.com/oxynote/oxynote/datagen`. Demo/testing only.
|
||||
- `e2e/` — Playwright end-to-end suite (`@oxynote/e2e`, **pnpm**) plus the docker-compose stack it drives. Not shipped; it exercises the composed product through a real backend built from this repo. Details: [e2e/CLAUDE.md](e2e/CLAUDE.md).
|
||||
- `scripts/` — helpers the root Makefile calls. `run-quietly.sh` runs a build step with its output held back, replaying the log only if the step fails.
|
||||
- `docker/` — dev docker-compose stack, Caddyfile, `env/` (committed `*.example.env` templates; `make setup` copies them to the gitignored `*.local.env` files the compose stack reads, and `web.example.env` also to `web/.env` for the host dev server and electron builds), `demo/` (demo-data configs for mariadb/postgres/prometheus).
|
||||
- `docker/` — dev docker-compose stack, Caddyfile, `env/` (committed `*.example.env` templates; `make setup` copies them to the gitignored `*.local.env` files the compose stack reads, and `web.example.env` also to `web/.env` for the host dev server and electron builds), `demo/` (demo-data configs for mariadb/postgres).
|
||||
|
||||
**One `.gitignore`, and it lives at the repository root.** Components do not carry their own — a rule for a nested directory is written with its path (`e2e/test-results/`, `web/coverage/`), so every exclusion in the repository is readable in one file. The exception is `web/packages/lezer-promql/`, which is a vendored upstream fork and keeps the ignore file it ships with.
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ make start # build images + run the dev stack in the background
|
|||
make dev # backend containers + web dev server on the host (hot reload, :3000)
|
||||
make stop # stop the dev stack
|
||||
|
||||
make lint # fix lint/format/type issues in web, auth-realtime, core, e2e
|
||||
make lint # fix lint/format/type issues in web, auth-realtime, core, datagen, e2e
|
||||
make check-lint # the same gates, verification only
|
||||
|
||||
make e2e # one-shot: build, run playwright, tear the e2e stack down
|
||||
|
|
|
|||
9
Makefile
9
Makefile
|
|
@ -54,15 +54,15 @@ e2e-stack-build:
|
|||
e2e-stack-stop:
|
||||
@$(QUIET) "stopping the stack" $(E2E_COMPOSE) down -v
|
||||
|
||||
# lint gates of every component that has one, from one place. Fixing and
|
||||
# checking mirror each component's own vocabulary: web, auth-realtime and
|
||||
# e2e pair lint/check-lint, and core wraps golangci-lint. Datagen has no
|
||||
# lint setup and is deliberately absent.
|
||||
# lint gates of every component, from one place. Fixing and checking mirror
|
||||
# each component's own vocabulary: web, auth-realtime and e2e pair
|
||||
# lint/check-lint, and core and datagen wrap golangci-lint.
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@$(QUIET) "linting web" sh -c 'cd web && pnpm run lint'
|
||||
@$(QUIET) "linting auth-realtime" sh -c 'cd server/auth-realtime && pnpm run lint'
|
||||
@$(QUIET) "linting core" sh -c 'cd server/core && make lint'
|
||||
@$(QUIET) "linting datagen" sh -c 'cd datagen && make lint'
|
||||
@$(QUIET) "linting e2e" sh -c 'cd e2e && pnpm run lint'
|
||||
|
||||
.PHONY: check-lint
|
||||
|
|
@ -70,6 +70,7 @@ check-lint:
|
|||
@$(QUIET) "checking web" sh -c 'cd web && pnpm run check-lint'
|
||||
@$(QUIET) "checking auth-realtime" sh -c 'cd server/auth-realtime && pnpm run check-lint'
|
||||
@$(QUIET) "checking core" sh -c 'cd server/core && make check-lint'
|
||||
@$(QUIET) "checking datagen" sh -c 'cd datagen && make check-lint'
|
||||
@$(QUIET) "checking e2e" sh -c 'cd e2e && pnpm run check-lint'
|
||||
|
||||
.PHONY: setup
|
||||
|
|
|
|||
324
datagen/.golangci.yml
Normal file
324
datagen/.golangci.yml
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
version: "2"
|
||||
linters:
|
||||
enable:
|
||||
- arangolint
|
||||
- asasalint
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- canonicalheader
|
||||
- clickhouselint
|
||||
- contextcheck
|
||||
- copyloopvar
|
||||
- cyclop
|
||||
- depguard
|
||||
- dogsled
|
||||
- dupword
|
||||
- durationcheck
|
||||
- embeddedstructfieldcheck
|
||||
- err113
|
||||
- errcheck
|
||||
- errchkjson
|
||||
- errname
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- exptostd
|
||||
- fatcontext
|
||||
- forbidigo
|
||||
- forcetypeassert
|
||||
- ginkgolinter
|
||||
- gocheckcompilerdirectives
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gochecksumtype
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godoclint
|
||||
- godot
|
||||
- gomoddirectives
|
||||
- gomodguard_v2
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- govet
|
||||
- importas
|
||||
- ineffassign
|
||||
- inamedparam
|
||||
- interfacebloat
|
||||
- intrange
|
||||
- iotamixing
|
||||
- loggercheck
|
||||
- maintidx
|
||||
- mirror
|
||||
- misspell
|
||||
- mnd
|
||||
- modernize
|
||||
- musttag
|
||||
- nakedret
|
||||
- nestif
|
||||
- nilerr
|
||||
- nilnesserr
|
||||
- nilnil
|
||||
- noctx
|
||||
- nolintlint
|
||||
- nosprintfhostport
|
||||
- paralleltest
|
||||
- perfsprint
|
||||
- prealloc
|
||||
- predeclared
|
||||
- promlinter
|
||||
- protogetter
|
||||
- reassign
|
||||
- recvcheck
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sloglint
|
||||
- spancheck
|
||||
- sqlclosecheck
|
||||
- staticcheck
|
||||
- tagalign
|
||||
- tagliatelle
|
||||
- testableexamples
|
||||
- testifylint
|
||||
- thelper
|
||||
- unconvert
|
||||
- unparam
|
||||
- unqueryvet
|
||||
- unused
|
||||
- usestdlibvars
|
||||
- usetesting
|
||||
- wastedassign
|
||||
- whitespace
|
||||
- wsl_v5
|
||||
- zerologlint
|
||||
disable:
|
||||
- containedctx
|
||||
- decorder
|
||||
- dupl
|
||||
- exhaustruct
|
||||
- funcorder
|
||||
- funlen
|
||||
- godox
|
||||
- goheader
|
||||
- gosmopolitan
|
||||
- grouper
|
||||
- iface
|
||||
- ireturn
|
||||
- lll
|
||||
- makezero
|
||||
- nlreturn
|
||||
- noinlineerr
|
||||
- nonamedreturns
|
||||
- testpackage
|
||||
- tparallel
|
||||
- varnamelen
|
||||
- wrapcheck
|
||||
settings:
|
||||
cyclop:
|
||||
max-complexity: 25
|
||||
errcheck:
|
||||
check-blank: true
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
deny:
|
||||
- pkg: gotest.tools/assert
|
||||
- pkg: github.com/bmizerany/assert
|
||||
exhaustive:
|
||||
default-signifies-exhaustive: true
|
||||
goconst:
|
||||
min-occurrences: 5
|
||||
numbers: true
|
||||
gomoddirectives:
|
||||
# datagen builds against the core module in this same repository, so
|
||||
# the local replace is how the two are wired, not a leftover.
|
||||
replace-local: true
|
||||
forbidigo:
|
||||
forbid:
|
||||
# timeutil.Now is mockable; time.Now is not, which makes every
|
||||
# clock-dependent path untestable through it.
|
||||
- pattern: ^time\.Now$
|
||||
msg: use timeutil.Now instead
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- hugeParam
|
||||
- unnamedResult
|
||||
- sloppyReassign
|
||||
- rangeValCopy
|
||||
enabled-tags:
|
||||
- diagnostic
|
||||
- style
|
||||
- performance
|
||||
- experimental
|
||||
- opinionated
|
||||
govet:
|
||||
disable:
|
||||
- fieldalignment
|
||||
enable-all: true
|
||||
nakedret:
|
||||
max-func-lines: 20
|
||||
paralleltest:
|
||||
ignore-missing: true
|
||||
prealloc:
|
||||
for-loops: true
|
||||
promlinter:
|
||||
strict: true
|
||||
recvcheck:
|
||||
exclusions:
|
||||
- '*.Value'
|
||||
- '*.Scan'
|
||||
revive:
|
||||
enable-all-rules: true
|
||||
rules:
|
||||
- name: unchecked-type-assertion
|
||||
disabled: true
|
||||
- name: import-alias-naming
|
||||
disabled: true
|
||||
- name: unnecessary-stmt
|
||||
disabled: true
|
||||
- name: argument-limit
|
||||
disabled: true
|
||||
- name: function-result-limit
|
||||
disabled: true
|
||||
- name: cyclomatic
|
||||
severity: warning
|
||||
disabled: true
|
||||
- name: line-length-limit
|
||||
severity: warning
|
||||
disabled: true
|
||||
- name: cognitive-complexity
|
||||
severity: warning
|
||||
disabled: true
|
||||
- name: confusing-naming
|
||||
disabled: true
|
||||
- name: confusing-results
|
||||
disabled: true
|
||||
- name: unhandled-error
|
||||
disabled: true
|
||||
- name: max-public-structs
|
||||
disabled: true
|
||||
- name: function-length
|
||||
disabled: true
|
||||
- name: banned-characters
|
||||
disabled: true
|
||||
- name: file-header
|
||||
disabled: true
|
||||
- name: add-constant
|
||||
disabled: true
|
||||
- name: comment-spacings
|
||||
disabled: true
|
||||
- name: unused-receiver
|
||||
disabled: true
|
||||
- name: nested-structs
|
||||
disabled: true
|
||||
- name: flag-parameter
|
||||
disabled: true
|
||||
- name: empty-lines
|
||||
disabled: true
|
||||
- name: if-return
|
||||
disabled: true
|
||||
- name: enforce-switch-style
|
||||
disabled: true
|
||||
rowserrcheck:
|
||||
packages:
|
||||
- github.com/jmoiron/sqlx
|
||||
tagalign:
|
||||
align: false
|
||||
sort: true
|
||||
order:
|
||||
- json
|
||||
- db
|
||||
tagliatelle:
|
||||
case:
|
||||
rules:
|
||||
json: camel
|
||||
testifylint:
|
||||
disable:
|
||||
- require-error
|
||||
thelper:
|
||||
test:
|
||||
begin: false
|
||||
wsl_v5:
|
||||
allow-first-in-block: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
rules:
|
||||
- linters:
|
||||
- forbidigo
|
||||
path: cmd
|
||||
text: fmt.Print
|
||||
- linters:
|
||||
- forbidigo
|
||||
path: _test.go
|
||||
text: time.Now
|
||||
- linters:
|
||||
- gochecknoglobals
|
||||
text: ^_.+
|
||||
- linters:
|
||||
- forcetypeassert
|
||||
- gochecknoinits
|
||||
- maintidx
|
||||
- promlinter
|
||||
- usestdlibvars
|
||||
path: _test.go
|
||||
- linters:
|
||||
- gocritic
|
||||
text: 'unnecessaryDefer: defer'
|
||||
- linters:
|
||||
- err113
|
||||
text: do not define dynamic errors
|
||||
- linters:
|
||||
- gosec
|
||||
path: _test.go
|
||||
text: G112
|
||||
- linters:
|
||||
- gosec
|
||||
text: G115
|
||||
- linters:
|
||||
- musttag
|
||||
text: should be annotated with the `db`
|
||||
- linters:
|
||||
- goconst
|
||||
text: string `(true|false)` has
|
||||
# the gauge tables are tuned sampling parameters read as a block: every
|
||||
# bound, target and spike chance is one row of demo tuning, not a
|
||||
# threshold logic branches on. Naming each would bury the tables, and a
|
||||
# shared constant would tie together gauges that only coincide today.
|
||||
- linters:
|
||||
- goconst
|
||||
- mnd
|
||||
path: internal/gen
|
||||
- linters:
|
||||
- dupword
|
||||
source: '^//go:generate '
|
||||
- linters:
|
||||
- usetesting
|
||||
text: could be replaced by t.Context()
|
||||
- linters:
|
||||
- revive
|
||||
text: Import alias "v1" is redundant
|
||||
- linters:
|
||||
- cyclop
|
||||
- goconst
|
||||
- noctx
|
||||
path: (.+)_test\.go
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gci
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goimports
|
||||
settings:
|
||||
gofumpt:
|
||||
extra-rules: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
# Minimum total statement coverage. Set from the measured baseline and
|
||||
# ratcheted up as suites grow — never lowered.
|
||||
COVERAGE_MIN = 92.9
|
||||
|
||||
# Build the Docker image
|
||||
# dockers_v2 snapshot builds load into the local daemon with an arch suffix;
|
||||
# retag so the dev compose stack can reference a stable :dev tag.
|
||||
|
|
@ -5,3 +9,25 @@
|
|||
build:
|
||||
goreleaser release --snapshot --clean
|
||||
docker tag ghcr.io/oxynote/datagen:dev-amd64 ghcr.io/oxynote/datagen:dev
|
||||
|
||||
# Run all tests with the race detector.
|
||||
.PHONY: test
|
||||
test:
|
||||
@go test -race ./...
|
||||
|
||||
# Run all tests with coverage and fail below the minimum.
|
||||
.PHONY: check-coverage
|
||||
check-coverage:
|
||||
@go test -race -coverprofile=coverage.out ./...
|
||||
@go tool cover -func=coverage.out | tail -n 1 | awk -v min=$(COVERAGE_MIN) \
|
||||
'{ print; if ($$3 + 0 < min) { printf "total coverage is below the %s%% minimum\n", min; exit 1 } }'
|
||||
|
||||
# Run the linters and apply automatic fixes.
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@golangci-lint run --fix --timeout 5m
|
||||
|
||||
# Run the linters without modifying anything.
|
||||
.PHONY: check-lint
|
||||
check-lint:
|
||||
@golangci-lint run --timeout 5m
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// Package main runs the demo data generator: it serves mock engineering
|
||||
// metrics for Prometheus to scrape and fills the demo databases with rows.
|
||||
package main
|
||||
|
||||
import (
|
||||
|
|
|
|||
21
datagen/cmd/main_test.go
Normal file
21
datagen/cmd/main_test.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
func Test_newLogger(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
log := newLogger()
|
||||
require.NotNil(t, log)
|
||||
assert.NotNil(t, log.Handler())
|
||||
}
|
||||
|
|
@ -6,27 +6,63 @@ require (
|
|||
github.com/go-chi/chi/v5 v5.3.2
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/jackc/pgx/v5 v5.10.0
|
||||
github.com/orlangure/gnomock v0.32.0
|
||||
github.com/oxynote/oxynote/server/core v0.0.0
|
||||
github.com/prometheus/client_golang v1.24.1
|
||||
github.com/prometheus/client_model v0.6.2
|
||||
github.com/stretchr/testify v1.12.1
|
||||
go.uber.org/goleak v1.3.0
|
||||
)
|
||||
|
||||
replace github.com/oxynote/oxynote/server/core => ../server/core
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/getsentry/sentry-go v0.46.2 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/docker v28.5.2+incompatible // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.1.0 // indirect
|
||||
github.com/getsentry/sentry-go v0.48.0 // indirect
|
||||
github.com/go-logr/logr v1.4.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/schema v1.4.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jarcoal/httpmock v1.4.2 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/common v0.70.1 // indirect
|
||||
github.com/prometheus/procfs v0.21.1 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 // indirect
|
||||
go.opentelemetry.io/otel v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.46.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.28.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
google.golang.org/grpc v1.83.0 // indirect
|
||||
google.golang.org/protobuf v1.36.12 // indirect
|
||||
)
|
||||
|
|
|
|||
115
datagen/go.sum
115
datagen/go.sum
|
|
@ -1,24 +1,55 @@
|
|||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
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/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/getsentry/sentry-go v0.46.2 h1:1jhYwrKGa3sIpo/y5iDNXS5wDoT7I1KNzMHrnK6ojns=
|
||||
github.com/getsentry/sentry-go v0.46.2/go.mod h1:evVbw2qotNUdYG8KxXbAdjOQWWvWIwKxpjdZZIvcIPw=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
|
||||
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
|
||||
github.com/getsentry/sentry-go v0.48.0 h1:FRZNr7Uk1C86ev1bSJmYlUkL9oyivQA6YOcdYfaaMmY=
|
||||
github.com/getsentry/sentry-go v0.48.0/go.mod h1:E5UkA5wp1qR2+MDydNYlVeUiNN2xEdjYMidkgf0Qoss=
|
||||
github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY=
|
||||
github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||
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/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
||||
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
|
|
@ -29,19 +60,37 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo
|
|||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jarcoal/httpmock v1.4.2 h1:dKwiP/9zITCPfBLsDn3kchbSOu16JrnxtVEmL0fPRcI=
|
||||
github.com/jarcoal/httpmock v1.4.2/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
||||
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
||||
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
|
||||
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
|
||||
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/orlangure/gnomock v0.32.0 h1:96KCsqbDUaKz2nKkGqC0nIlJeCRaSfEpSi4CljBp8zk=
|
||||
github.com/orlangure/gnomock v0.32.0/go.mod h1:CpMbwyCmPFpeLrsA5LIUcMrGm7LOf9+2JE+taayrUPc=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
|
||||
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
|
|
@ -54,24 +103,62 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
|||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
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/contrib/instrumentation/net/http/otelhttp v0.70.0 h1:LMuyCAyfalSjDyjdC65nK6N0zoTT63+E/u95X0JovZI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0/go.mod h1:085m8qbm4hgc8rZWGDEa4vmyyo2c3nPxUslYUKUIU04=
|
||||
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
|
||||
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 h1:Mbi5PKN7u322woPa85d7ebZ+SOvEoPvoiBu+ryHWgfA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0/go.mod h1:e7ciERRhZaOZXVjx5MiL8TK5+Xv7G5Gv5PA2ZDEJdL8=
|
||||
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
|
||||
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
|
||||
go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
|
||||
go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
|
||||
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
|
||||
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
|
||||
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/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
|
||||
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ=
|
||||
google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
|
||||
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// Package demodata synthesises the deployment, incident and build rows the
|
||||
// demo data sources are filled with.
|
||||
package demodata
|
||||
|
||||
import (
|
||||
|
|
@ -8,46 +10,153 @@ import (
|
|||
"github.com/oxynote/oxynote/datagen/internal/mockmetrics"
|
||||
)
|
||||
|
||||
// TickInterval defines how much demo time a single generated tick covers.
|
||||
const TickInterval = 5 * time.Minute
|
||||
|
||||
var (
|
||||
services = []string{"api-gateway", "user-service", "billing-service", "notification-service", "search-service"}
|
||||
environments = []string{"production", "staging"}
|
||||
severities = []string{"critical", "major", "minor"}
|
||||
repositories = []string{"frontend", "backend", "infra", "mobile", "shared-libs"}
|
||||
branches = []string{"main", "develop", "feature"}
|
||||
// Deployment sampling parameters.
|
||||
const (
|
||||
// _deploySuccessRate is the share of deployments that succeed outside
|
||||
// Friday afternoons.
|
||||
_deploySuccessRate = 0.92
|
||||
|
||||
// _deployFridaySuccessRate is that share once Friday afternoon starts.
|
||||
_deployFridaySuccessRate = 0.78
|
||||
|
||||
// _deployRollbackRate is the share of failed deployments rolled back.
|
||||
_deployRollbackRate = 0.6
|
||||
|
||||
// _deployDurationMean and _deployDurationStdDev shape how long a
|
||||
// deployment takes, in seconds.
|
||||
_deployDurationMean = 120
|
||||
_deployDurationStdDev = 45
|
||||
)
|
||||
|
||||
// Incident sampling parameters.
|
||||
const (
|
||||
// _incidentDetectRelStdDev spreads time-to-detect around its severity's
|
||||
// mean, as a fraction of that mean.
|
||||
_incidentDetectRelStdDev = 0.4
|
||||
|
||||
// _incidentResolveRelStdDev does the same for time-to-resolve.
|
||||
_incidentResolveRelStdDev = 0.3
|
||||
)
|
||||
|
||||
// Build sampling parameters.
|
||||
const (
|
||||
// _buildFailureRate is the share of builds that fail any tests at all.
|
||||
_buildFailureRate = 0.15
|
||||
|
||||
// _buildFailedTestsMean is the average number of tests that fail when a
|
||||
// build fails.
|
||||
_buildFailedTestsMean = 3
|
||||
|
||||
// _buildDurationMean and _buildDurationStdDev shape how long a build
|
||||
// takes, in seconds.
|
||||
_buildDurationMean = 180
|
||||
_buildDurationStdDev = 60
|
||||
|
||||
// _buildTestCountMean and _buildTestCountRelStdDev shape how many tests
|
||||
// a build runs.
|
||||
_buildTestCountMean = 850
|
||||
_buildTestCountRelStdDev = 0.1
|
||||
|
||||
// _buildCoverageMean and _buildCoverageStdDev shape reported coverage,
|
||||
// which is then held within [_buildCoverageMin, _buildCoverageMax].
|
||||
_buildCoverageMean = 72
|
||||
_buildCoverageStdDev = 8
|
||||
_buildCoverageMin = 20
|
||||
_buildCoverageMax = 98
|
||||
)
|
||||
|
||||
var (
|
||||
// _services holds the service names demo rows are attributed to.
|
||||
_services = []string{"api-gateway", "user-service", "billing-service", "notification-service", "search-service"}
|
||||
|
||||
// _environments holds the environments demo deployments target.
|
||||
_environments = []string{"production", "staging"}
|
||||
|
||||
// _severities holds the severity levels demo incidents are raised at.
|
||||
_severities = []string{"critical", "major", "minor"}
|
||||
|
||||
// _repositories holds the repository names demo builds run against.
|
||||
_repositories = []string{"frontend", "backend", "infra", "mobile", "shared-libs"}
|
||||
|
||||
// _branches holds the branch names demo builds run on.
|
||||
_branches = []string{"main", "develop", "feature"}
|
||||
)
|
||||
|
||||
// Deployment specifies a single demo deployment event.
|
||||
type Deployment struct {
|
||||
Time time.Time
|
||||
Service string
|
||||
Environment string
|
||||
// Time specifies when the deployment happened.
|
||||
Time time.Time
|
||||
|
||||
// Service specifies the deployed service.
|
||||
Service string
|
||||
|
||||
// Environment specifies the environment deployed to.
|
||||
Environment string
|
||||
|
||||
// DurationSeconds specifies how long the deployment took.
|
||||
DurationSeconds float64
|
||||
Success bool
|
||||
Rollback bool
|
||||
|
||||
// Success indicates whether the deployment succeeded.
|
||||
Success bool
|
||||
|
||||
// Rollback indicates whether the deployment was rolled back.
|
||||
Rollback bool
|
||||
}
|
||||
|
||||
// Incident specifies a single demo incident event.
|
||||
type Incident struct {
|
||||
Time time.Time
|
||||
Severity string
|
||||
Service string
|
||||
TimeToDetectMinutes float64
|
||||
TimeToResolveMinutes float64
|
||||
// Time specifies when the incident started.
|
||||
Time time.Time
|
||||
|
||||
// Severity specifies how severe the incident was.
|
||||
Severity string
|
||||
|
||||
// Service specifies the affected service.
|
||||
Service string
|
||||
|
||||
// TimeToDetectMinutes specifies how long the incident went unnoticed.
|
||||
TimeToDetectMinutes float64
|
||||
|
||||
// TimeToResolveMinutes specifies how long the incident took to resolve.
|
||||
TimeToResolveMinutes float64
|
||||
}
|
||||
|
||||
// BuildMetric specifies a single demo CI build result.
|
||||
type BuildMetric struct {
|
||||
Time time.Time
|
||||
Repository string
|
||||
Branch string
|
||||
// Time specifies when the build ran.
|
||||
Time time.Time
|
||||
|
||||
// Repository specifies the built repository.
|
||||
Repository string
|
||||
|
||||
// Branch specifies the built branch.
|
||||
Branch string
|
||||
|
||||
// DurationSeconds specifies how long the build took.
|
||||
DurationSeconds float64
|
||||
TestCount int
|
||||
TestsFailed int
|
||||
CoveragePct float64
|
||||
|
||||
// TestCount specifies how many tests the build ran.
|
||||
TestCount int
|
||||
|
||||
// TestsFailed specifies how many of those tests failed.
|
||||
TestsFailed int
|
||||
|
||||
// CoveragePct specifies the coverage percentage the build reported.
|
||||
CoveragePct float64
|
||||
}
|
||||
|
||||
// Tick specifies everything generated for a single interval.
|
||||
type Tick struct {
|
||||
Deployments []Deployment
|
||||
Incidents []Incident
|
||||
// Deployments holds the deployments generated for the tick.
|
||||
Deployments []Deployment
|
||||
|
||||
// Incidents holds the incidents generated for the tick.
|
||||
Incidents []Incident
|
||||
|
||||
// BuildMetrics holds the build results generated for the tick.
|
||||
BuildMetrics []BuildMetric
|
||||
}
|
||||
|
||||
|
|
@ -60,6 +169,8 @@ func GenerateTick(r *rand.Rand, t time.Time) Tick {
|
|||
}
|
||||
}
|
||||
|
||||
// genDeployments generates the deployments of a single tick, at a rate that
|
||||
// peaks during weekday office hours and again on Friday afternoons.
|
||||
func genDeployments(r *rand.Rand, t time.Time) []Deployment {
|
||||
hour := t.Hour()
|
||||
weekday := t.Weekday()
|
||||
|
|
@ -80,26 +191,28 @@ func genDeployments(r *rand.Rand, t time.Time) []Deployment {
|
|||
out := make([]Deployment, 0, count)
|
||||
|
||||
for range count {
|
||||
success := r.Float64() < 0.92
|
||||
success := r.Float64() < _deploySuccessRate
|
||||
if weekday == time.Friday && hour >= 15 {
|
||||
success = r.Float64() < 0.78
|
||||
success = r.Float64() < _deployFridaySuccessRate
|
||||
}
|
||||
|
||||
jitter := time.Duration(r.Intn(int(TickInterval.Seconds()))) * time.Second
|
||||
|
||||
out = append(out, Deployment{
|
||||
Time: t.Add(jitter),
|
||||
Service: services[r.Intn(len(services))],
|
||||
Environment: environments[r.Intn(len(environments))],
|
||||
DurationSeconds: math.Abs(mockmetrics.Normal(r, 120, 45)),
|
||||
Service: _services[r.Intn(len(_services))],
|
||||
Environment: _environments[r.Intn(len(_environments))],
|
||||
DurationSeconds: math.Abs(mockmetrics.Normal(r, _deployDurationMean, _deployDurationStdDev)),
|
||||
Success: success,
|
||||
Rollback: !success && r.Float64() < 0.6,
|
||||
Rollback: !success && r.Float64() < _deployRollbackRate,
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// genIncidents generates the incidents of a single tick, at a rate that rises
|
||||
// on weekdays and spikes on Friday afternoons.
|
||||
func genIncidents(r *rand.Rand, t time.Time) []Incident {
|
||||
hour := t.Hour()
|
||||
weekday := t.Weekday()
|
||||
|
|
@ -116,7 +229,7 @@ func genIncidents(r *rand.Rand, t time.Time) []Incident {
|
|||
out := make([]Incident, 0, count)
|
||||
|
||||
for range count {
|
||||
severity := severities[r.Intn(len(severities))]
|
||||
severity := _severities[r.Intn(len(_severities))]
|
||||
|
||||
var detectMean, resolveMean float64
|
||||
|
||||
|
|
@ -132,17 +245,19 @@ func genIncidents(r *rand.Rand, t time.Time) []Incident {
|
|||
jitter := time.Duration(r.Intn(int(TickInterval.Seconds()))) * time.Second
|
||||
|
||||
out = append(out, Incident{
|
||||
Time: t.Add(jitter),
|
||||
Severity: severity,
|
||||
Service: services[r.Intn(len(services))],
|
||||
TimeToDetectMinutes: math.Abs(mockmetrics.Normal(r, detectMean, detectMean*0.4)),
|
||||
TimeToResolveMinutes: math.Abs(mockmetrics.Normal(r, resolveMean, resolveMean*0.3)),
|
||||
Time: t.Add(jitter),
|
||||
Severity: severity,
|
||||
Service: _services[r.Intn(len(_services))],
|
||||
TimeToDetectMinutes: math.Abs(mockmetrics.Normal(r, detectMean, detectMean*_incidentDetectRelStdDev)),
|
||||
TimeToResolveMinutes: math.Abs(mockmetrics.Normal(r, resolveMean, resolveMean*_incidentResolveRelStdDev)),
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// genBuildMetrics generates the CI builds of a single tick, at a rate that
|
||||
// peaks during weekday office hours.
|
||||
func genBuildMetrics(r *rand.Rand, t time.Time) []BuildMetric {
|
||||
hour := t.Hour()
|
||||
weekday := t.Weekday()
|
||||
|
|
@ -160,20 +275,24 @@ func genBuildMetrics(r *rand.Rand, t time.Time) []BuildMetric {
|
|||
|
||||
for range count {
|
||||
testsFailed := 0
|
||||
if r.Float64() < 0.15 {
|
||||
testsFailed = mockmetrics.Poisson(r, 3)
|
||||
if r.Float64() < _buildFailureRate {
|
||||
testsFailed = mockmetrics.Poisson(r, _buildFailedTestsMean)
|
||||
}
|
||||
|
||||
jitter := time.Duration(r.Intn(int(TickInterval.Seconds()))) * time.Second
|
||||
|
||||
out = append(out, BuildMetric{
|
||||
Time: t.Add(jitter),
|
||||
Repository: repositories[r.Intn(len(repositories))],
|
||||
Branch: branches[r.Intn(len(branches))],
|
||||
DurationSeconds: math.Abs(mockmetrics.Normal(r, 180, 60)),
|
||||
TestCount: mockmetrics.RandCount(r, 850, 0.1),
|
||||
Repository: _repositories[r.Intn(len(_repositories))],
|
||||
Branch: _branches[r.Intn(len(_branches))],
|
||||
DurationSeconds: math.Abs(mockmetrics.Normal(r, _buildDurationMean, _buildDurationStdDev)),
|
||||
TestCount: mockmetrics.RandCount(r, _buildTestCountMean, _buildTestCountRelStdDev),
|
||||
TestsFailed: testsFailed,
|
||||
CoveragePct: mockmetrics.Clamp(mockmetrics.Normal(r, 72, 8), 20, 98),
|
||||
CoveragePct: mockmetrics.Clamp(
|
||||
mockmetrics.Normal(r, _buildCoverageMean, _buildCoverageStdDev),
|
||||
_buildCoverageMin,
|
||||
_buildCoverageMax,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
262
datagen/internal/demodata/demodata_test.go
Normal file
262
datagen/internal/demodata/demodata_test.go
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
package demodata
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/oxynote/oxynote/datagen/internal/mockmetrics"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
// Reference instants for each rate branch the generators switch on.
|
||||
var (
|
||||
// _sunday is outside the working week entirely.
|
||||
_sunday = time.Date(2024, time.January, 7, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
// _tuesdayNight is a weekday outside office hours.
|
||||
_tuesdayNight = time.Date(2024, time.January, 9, 3, 0, 0, 0, time.UTC)
|
||||
|
||||
// _tuesdayOffice is a weekday inside office hours.
|
||||
_tuesdayOffice = time.Date(2024, time.January, 9, 11, 0, 0, 0, time.UTC)
|
||||
|
||||
// _fridayAfternoon is when everything gets worse.
|
||||
_fridayAfternoon = time.Date(2024, time.January, 12, 16, 0, 0, 0, time.UTC)
|
||||
)
|
||||
|
||||
func Test_GenerateTick(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// a tick carries whatever each generator produced for the same instant.
|
||||
tick := GenerateTick(mockmetrics.NewRand(1), _tuesdayOffice)
|
||||
|
||||
assert.Equal(t, genDeployments(mockmetrics.NewRand(1), _tuesdayOffice), tick.Deployments)
|
||||
|
||||
// the generators share one source, so replaying them in order is the
|
||||
// only way to reproduce the later two.
|
||||
r := mockmetrics.NewRand(1)
|
||||
genDeployments(r, _tuesdayOffice)
|
||||
|
||||
assert.Equal(t, genIncidents(r, _tuesdayOffice), tick.Incidents)
|
||||
assert.Equal(t, genBuildMetrics(r, _tuesdayOffice), tick.BuildMetrics)
|
||||
}
|
||||
|
||||
func Test_genDeployments(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
Time time.Time
|
||||
|
||||
// MinRows is the number of rows the case must produce across its
|
||||
// draws for the branch to count as exercised.
|
||||
MinRows int
|
||||
}{
|
||||
"Weekend runs at the lowest rate": {
|
||||
Time: _sunday,
|
||||
MinRows: 1,
|
||||
},
|
||||
"Weekday outside office hours": {
|
||||
Time: _tuesdayNight,
|
||||
MinRows: 1,
|
||||
},
|
||||
"Weekday office hours": {
|
||||
Time: _tuesdayOffice,
|
||||
MinRows: 1,
|
||||
},
|
||||
"Friday afternoon": {
|
||||
Time: _fridayAfternoon,
|
||||
MinRows: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
r = mockmetrics.NewRand(1)
|
||||
rows = make([]Deployment, 0, 200)
|
||||
)
|
||||
|
||||
for range 200 {
|
||||
rows = append(rows, genDeployments(r, c.Time)...)
|
||||
}
|
||||
|
||||
require.GreaterOrEqual(t, len(rows), c.MinRows)
|
||||
|
||||
for _, d := range rows {
|
||||
assert.Contains(t, _services, d.Service)
|
||||
assert.Contains(t, _environments, d.Environment)
|
||||
assert.GreaterOrEqual(t, d.DurationSeconds, 0.0)
|
||||
assertJittered(t, c.Time, d.Time)
|
||||
|
||||
// a successful deployment is never rolled back.
|
||||
if d.Success {
|
||||
assert.False(t, d.Rollback)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// the branches exist to make busier periods busier, so the row counts
|
||||
// have to come out ordered.
|
||||
assert.Less(t, countDeployments(t, _sunday), countDeployments(t, _tuesdayOffice))
|
||||
assert.Less(t, countDeployments(t, _tuesdayNight), countDeployments(t, _tuesdayOffice))
|
||||
assert.Less(t, countDeployments(t, _tuesdayOffice), countDeployments(t, _fridayAfternoon))
|
||||
}
|
||||
|
||||
func Test_genIncidents(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
Time time.Time
|
||||
}{
|
||||
"Weekend runs at the lowest rate": {Time: _sunday},
|
||||
"Weekday": {Time: _tuesdayOffice},
|
||||
"Friday afternoon": {Time: _fridayAfternoon},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
r = mockmetrics.NewRand(1)
|
||||
rows = make([]Incident, 0, 500)
|
||||
)
|
||||
|
||||
for range 500 {
|
||||
rows = append(rows, genIncidents(r, c.Time)...)
|
||||
}
|
||||
|
||||
require.NotEmpty(t, rows)
|
||||
|
||||
var seen []string
|
||||
|
||||
for _, i := range rows {
|
||||
assert.Contains(t, _severities, i.Severity)
|
||||
assert.Contains(t, _services, i.Service)
|
||||
assert.GreaterOrEqual(t, i.TimeToDetectMinutes, 0.0)
|
||||
assert.GreaterOrEqual(t, i.TimeToResolveMinutes, 0.0)
|
||||
assertJittered(t, c.Time, i.Time)
|
||||
|
||||
if !slices.Contains(seen, i.Severity) {
|
||||
seen = append(seen, i.Severity)
|
||||
}
|
||||
}
|
||||
|
||||
// every severity carries its own detect/resolve means, so the
|
||||
// draw has to cover all three branches of the switch.
|
||||
assert.ElementsMatch(t, _severities, seen)
|
||||
})
|
||||
}
|
||||
|
||||
// busier periods must come out busier.
|
||||
assert.Less(t, countIncidents(t, _sunday), countIncidents(t, _tuesdayOffice))
|
||||
assert.Less(t, countIncidents(t, _tuesdayOffice), countIncidents(t, _fridayAfternoon))
|
||||
}
|
||||
|
||||
func Test_genBuildMetrics(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
Time time.Time
|
||||
}{
|
||||
"Weekend runs at the lowest rate": {Time: _sunday},
|
||||
"Weekday outside office hours": {Time: _tuesdayNight},
|
||||
"Weekday office hours": {Time: _tuesdayOffice},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
r = mockmetrics.NewRand(1)
|
||||
rows = make([]BuildMetric, 0, 200)
|
||||
)
|
||||
|
||||
for range 200 {
|
||||
rows = append(rows, genBuildMetrics(r, c.Time)...)
|
||||
}
|
||||
|
||||
require.NotEmpty(t, rows)
|
||||
|
||||
var sawFailures bool
|
||||
|
||||
for _, b := range rows {
|
||||
assert.Contains(t, _repositories, b.Repository)
|
||||
assert.Contains(t, _branches, b.Branch)
|
||||
assert.GreaterOrEqual(t, b.DurationSeconds, 0.0)
|
||||
assert.GreaterOrEqual(t, b.TestCount, 0)
|
||||
assert.GreaterOrEqual(t, b.TestsFailed, 0)
|
||||
assert.GreaterOrEqual(t, b.CoveragePct, float64(_buildCoverageMin))
|
||||
assert.LessOrEqual(t, b.CoveragePct, float64(_buildCoverageMax))
|
||||
assertJittered(t, c.Time, b.Time)
|
||||
|
||||
if b.TestsFailed > 0 {
|
||||
sawFailures = true
|
||||
}
|
||||
}
|
||||
|
||||
// the failing-build branch is rare but has to be reached.
|
||||
assert.True(t, sawFailures)
|
||||
})
|
||||
}
|
||||
|
||||
// busier periods must come out busier.
|
||||
assert.Less(t, countBuilds(t, _sunday), countBuilds(t, _tuesdayOffice))
|
||||
assert.Less(t, countBuilds(t, _tuesdayNight), countBuilds(t, _tuesdayOffice))
|
||||
}
|
||||
|
||||
// assertJittered checks that a generated row landed inside the tick it was
|
||||
// generated for.
|
||||
func assertJittered(t *testing.T, tick, got time.Time) {
|
||||
t.Helper()
|
||||
|
||||
assert.False(t, got.Before(tick))
|
||||
assert.True(t, got.Before(tick.Add(TickInterval)))
|
||||
}
|
||||
|
||||
// countDeployments totals the deployments generated for one instant over
|
||||
// enough draws for the rates to separate.
|
||||
func countDeployments(t *testing.T, at time.Time) int {
|
||||
t.Helper()
|
||||
|
||||
return countRows(func(r *rand.Rand) int { return len(genDeployments(r, at)) })
|
||||
}
|
||||
|
||||
// countIncidents does the same for incidents.
|
||||
func countIncidents(t *testing.T, at time.Time) int {
|
||||
t.Helper()
|
||||
|
||||
return countRows(func(r *rand.Rand) int { return len(genIncidents(r, at)) })
|
||||
}
|
||||
|
||||
// countBuilds does the same for builds.
|
||||
func countBuilds(t *testing.T, at time.Time) int {
|
||||
t.Helper()
|
||||
|
||||
return countRows(func(r *rand.Rand) int { return len(genBuildMetrics(r, at)) })
|
||||
}
|
||||
|
||||
// countRows sums what fn generates over a fixed number of draws from a fixed
|
||||
// seed, so the totals of two instants are comparable.
|
||||
func countRows(fn func(*rand.Rand) int) int {
|
||||
r := mockmetrics.NewRand(1)
|
||||
|
||||
var total int
|
||||
|
||||
for range 2_000 {
|
||||
total += fn(r)
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// Package gen generates the mock engineering-culture metrics the demo
|
||||
// Prometheus scrapes.
|
||||
package gen
|
||||
|
||||
import (
|
||||
|
|
@ -8,12 +10,32 @@ import (
|
|||
|
||||
"github.com/oxynote/oxynote/datagen/internal/mockmetrics"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/metricutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/timeutil"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// _genInterval defines how often metrics are refreshed.
|
||||
const _genInterval = 5 * time.Minute
|
||||
|
||||
// _labelOutcome names the label both outcome-split gauges are broken down by.
|
||||
const _labelOutcome = "outcome"
|
||||
|
||||
// Sampling parameters of the friday_deploys gauge, whose two buckets are
|
||||
// derived from one another instead of being sampled independently.
|
||||
const (
|
||||
// _fridayDeployMean is the weekly number of Friday deploys attempted.
|
||||
_fridayDeployMean = 30_000
|
||||
|
||||
// _fridayDeployRelStdDev spreads that count tick to tick.
|
||||
_fridayDeployRelStdDev = 0.15
|
||||
|
||||
// _fridayDeploySuccessMean is the mean fraction of attempts that survive.
|
||||
_fridayDeploySuccessMean = 0.68
|
||||
|
||||
// _fridayDeploySuccessStdDev spreads that fraction tick to tick.
|
||||
_fridayDeploySuccessStdDev = 0.12
|
||||
)
|
||||
|
||||
// continuousMetric holds the running state for a single drifting gauge.
|
||||
type continuousMetric struct {
|
||||
// value is the current gauge reading, updated on each tick.
|
||||
|
|
@ -52,6 +74,9 @@ type Manager struct {
|
|||
// log is the structured logger.
|
||||
log *slog.Logger
|
||||
|
||||
// interval defines how often metrics are refreshed.
|
||||
interval time.Duration
|
||||
|
||||
// Continuous metric states — these drift smoothly between ticks.
|
||||
|
||||
// tempFixAge states by original_comment label — hacks live the longest.
|
||||
|
|
@ -137,15 +162,17 @@ type Manager struct {
|
|||
}
|
||||
|
||||
// NewManager creates a ready-to-run Manager.
|
||||
//
|
||||
//nolint:maintidx // a flat table of tuned gauge parameters, not logic
|
||||
func NewManager(fc metricutil.Factory, log *slog.Logger) *Manager {
|
||||
newGaugeVec := func(name, help string, labels ...string) metricutil.GaugeVec {
|
||||
return fc.NewGaugeVec(metricutil.Options{Name: name, Help: help}, labels)
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
r: mockmetrics.NewRand(time.Now().UnixNano()),
|
||||
log: log,
|
||||
|
||||
r: mockmetrics.NewRand(timeutil.Now().UnixNano()),
|
||||
log: log,
|
||||
interval: _genInterval,
|
||||
// Temporary fix age by comment type — hacks accumulate and never leave.
|
||||
tempFixAgeTemp: newContinuous(mockmetrics.GaugeParams{
|
||||
Min: 365,
|
||||
|
|
@ -370,7 +397,6 @@ func NewManager(fc metricutil.Factory, log *slog.Logger) *Manager {
|
|||
SpikeStdDev: 30_000,
|
||||
}),
|
||||
|
||||
// Prometheus gauges.
|
||||
meetingsCouldbDoc: newGaugeVec(
|
||||
"meetings_could_be_doc_count",
|
||||
"Weekly count of meetings that could've been a document.",
|
||||
|
|
@ -379,12 +405,12 @@ func NewManager(fc metricutil.Factory, log *slog.Logger) *Manager {
|
|||
circleBackCount: newGaugeVec(
|
||||
"circle_back_count",
|
||||
`Weekly count of "let's circle back" mentions.`,
|
||||
"outcome",
|
||||
_labelOutcome,
|
||||
),
|
||||
fridayDeploys: newGaugeVec(
|
||||
"friday_deploys_count",
|
||||
"Weekly Friday deploy counts.",
|
||||
"outcome",
|
||||
_labelOutcome,
|
||||
),
|
||||
featureFlagGrave: newGaugeVec(
|
||||
"feature_flag_graveyard_size",
|
||||
|
|
@ -449,16 +475,35 @@ func NewManager(fc metricutil.Factory, log *slog.Logger) *Manager {
|
|||
}
|
||||
|
||||
// Run starts the generation loop. It blocks until ctx is cancelled.
|
||||
func (m *Manager) Run(ctx context.Context) error {
|
||||
m.generateData()
|
||||
func (m *Manager) Run(ctx context.Context) {
|
||||
timeutil.NewPeriodicExec(
|
||||
m.interval,
|
||||
0,
|
||||
func(context.Context) { m.generateData() },
|
||||
nil,
|
||||
true,
|
||||
).Start(ctx)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-time.After(_genInterval):
|
||||
m.generateData()
|
||||
}
|
||||
// countBucket specifies the sampling parameters of one labelled bucket of a
|
||||
// gauge that is resampled from scratch on every tick.
|
||||
type countBucket struct {
|
||||
// label is the label value the bucket is published under.
|
||||
label string
|
||||
|
||||
// mean is the average count sampled for the bucket each tick.
|
||||
mean float64
|
||||
|
||||
// relStdDev is the bucket's standard deviation as a fraction of mean.
|
||||
relStdDev float64
|
||||
}
|
||||
|
||||
// setCounts resamples every bucket of a gauge and publishes the results
|
||||
// under the given label key.
|
||||
func (m *Manager) setCounts(vec metricutil.GaugeVec, key string, bb []countBucket) {
|
||||
for _, b := range bb {
|
||||
vec.With(prometheus.Labels{key: b.label}).
|
||||
Set(float64(mockmetrics.RandCount(m.r, b.mean, b.relStdDev)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -466,52 +511,72 @@ func (m *Manager) Run(ctx context.Context) error {
|
|||
func (m *Manager) generateData() {
|
||||
r := m.r
|
||||
|
||||
// --- Poisson count metrics (fresh each tick) ---
|
||||
// --- Count metrics (resampled from scratch each tick) ---
|
||||
|
||||
// Lambdas are aggregate weekly totals across ~10k organisations.
|
||||
// Means are aggregate weekly totals across ~10k organisations.
|
||||
|
||||
// meetings_could_be_doc: split by the excuse used to justify the meeting.
|
||||
m.meetingsCouldbDoc.With(prometheus.Labels{"excuse": "alignment"}).Set(float64(mockmetrics.RandCount(r, 42_000, 0.15)))
|
||||
m.meetingsCouldbDoc.With(prometheus.Labels{"excuse": "bandwidth"}).Set(float64(mockmetrics.RandCount(r, 36_000, 0.15)))
|
||||
m.meetingsCouldbDoc.With(prometheus.Labels{"excuse": "synergy"}).Set(float64(mockmetrics.RandCount(r, 30_000, 0.15)))
|
||||
m.meetingsCouldbDoc.With(prometheus.Labels{"excuse": "optics"}).Set(float64(mockmetrics.RandCount(r, 12_000, 0.20)))
|
||||
m.setCounts(m.meetingsCouldbDoc, "excuse", []countBucket{
|
||||
{label: "alignment", mean: 42_000, relStdDev: 0.15},
|
||||
{label: "bandwidth", mean: 36_000, relStdDev: 0.15},
|
||||
{label: "synergy", mean: 30_000, relStdDev: 0.15},
|
||||
{label: "optics", mean: 12_000, relStdDev: 0.20},
|
||||
})
|
||||
|
||||
// circle_back_count: split by what actually happened afterward.
|
||||
m.circleBackCount.With(prometheus.Labels{"outcome": "circled_back"}).Set(float64(mockmetrics.RandCount(r, 16_000, 0.20)))
|
||||
m.circleBackCount.With(prometheus.Labels{"outcome": "forgotten"}).Set(float64(mockmetrics.RandCount(r, 48_000, 0.15)))
|
||||
m.circleBackCount.With(prometheus.Labels{"outcome": "rescheduled_to_circle_back"}).Set(float64(mockmetrics.RandCount(r, 16_000, 0.20)))
|
||||
m.setCounts(m.circleBackCount, _labelOutcome, []countBucket{
|
||||
{label: "circled_back", mean: 16_000, relStdDev: 0.20},
|
||||
{label: "forgotten", mean: 48_000, relStdDev: 0.15},
|
||||
{label: "rescheduled_to_circle_back", mean: 16_000, relStdDev: 0.20},
|
||||
})
|
||||
|
||||
// friday_deploys: outcome (attempted vs successful).
|
||||
attempted := mockmetrics.RandCount(r, 30_000, 0.15)
|
||||
successRate := mockmetrics.Clamp(mockmetrics.Normal(r, 0.68, 0.12), 0, 1)
|
||||
// friday_deploys: the successful bucket is a fraction of the attempted
|
||||
// one rather than an independent sample, so it cannot join a table.
|
||||
attempted := mockmetrics.RandCount(r, _fridayDeployMean, _fridayDeployRelStdDev)
|
||||
successRate := mockmetrics.Clamp(
|
||||
mockmetrics.Normal(r, _fridayDeploySuccessMean, _fridayDeploySuccessStdDev),
|
||||
0,
|
||||
1,
|
||||
)
|
||||
successful := int(mockmetrics.Clamp(float64(attempted)*successRate, 0, float64(attempted)))
|
||||
m.fridayDeploys.With(prometheus.Labels{"outcome": "attempted"}).Set(float64(attempted))
|
||||
m.fridayDeploys.With(prometheus.Labels{"outcome": "successful"}).Set(float64(successful))
|
||||
|
||||
m.fridayDeploys.With(prometheus.Labels{_labelOutcome: "attempted"}).Set(float64(attempted))
|
||||
m.fridayDeploys.With(prometheus.Labels{_labelOutcome: "successful"}).Set(float64(successful))
|
||||
|
||||
// pizza_fridays: split by how many pizzas were ordered.
|
||||
m.pizzaFridays.With(prometheus.Labels{"pizza_count": "1_to_5"}).Set(float64(mockmetrics.RandCount(r, 10_000, 0.15)))
|
||||
m.pizzaFridays.With(prometheus.Labels{"pizza_count": "5_to_10"}).Set(float64(mockmetrics.RandCount(r, 7_000, 0.20)))
|
||||
m.pizzaFridays.With(prometheus.Labels{"pizza_count": "10_plus"}).Set(float64(mockmetrics.RandCount(r, 3_000, 0.25)))
|
||||
m.setCounts(m.pizzaFridays, "pizza_count", []countBucket{
|
||||
{label: "1_to_5", mean: 10_000, relStdDev: 0.15},
|
||||
{label: "5_to_10", mean: 7_000, relStdDev: 0.20},
|
||||
{label: "10_plus", mean: 3_000, relStdDev: 0.25},
|
||||
})
|
||||
|
||||
// ship_it_approvals: split by reviewer's apparent state of mind.
|
||||
m.shipItApprovals.With(prometheus.Labels{"reviewer_state": "sober"}).Set(float64(mockmetrics.RandCount(r, 20_000, 0.15)))
|
||||
m.shipItApprovals.With(prometheus.Labels{"reviewer_state": "one_beer_in"}).Set(float64(mockmetrics.RandCount(r, 14_000, 0.20)))
|
||||
m.shipItApprovals.With(prometheus.Labels{"reviewer_state": "heading_to_the_door"}).Set(float64(mockmetrics.RandCount(r, 6_000, 0.25)))
|
||||
m.setCounts(m.shipItApprovals, "reviewer_state", []countBucket{
|
||||
{label: "sober", mean: 20_000, relStdDev: 0.15},
|
||||
{label: "one_beer_in", mean: 14_000, relStdDev: 0.20},
|
||||
{label: "heading_to_the_door", mean: 6_000, relStdDev: 0.25},
|
||||
})
|
||||
|
||||
// prs_merged_friday: split by the deployer's stated confidence level.
|
||||
m.prsMergedFriday.With(prometheus.Labels{"confidence": "yolo"}).Set(float64(mockmetrics.RandCount(r, 20_000, 0.15)))
|
||||
m.prsMergedFriday.With(prometheus.Labels{"confidence": "fingers_crossed"}).Set(float64(mockmetrics.RandCount(r, 22_500, 0.15)))
|
||||
m.prsMergedFriday.With(prometheus.Labels{"confidence": "vacation_starts_monday"}).Set(float64(mockmetrics.RandCount(r, 7_500, 0.20)))
|
||||
m.setCounts(m.prsMergedFriday, "confidence", []countBucket{
|
||||
{label: "yolo", mean: 20_000, relStdDev: 0.15},
|
||||
{label: "fingers_crossed", mean: 22_500, relStdDev: 0.15},
|
||||
{label: "vacation_starts_monday", mean: 7_500, relStdDev: 0.20},
|
||||
})
|
||||
|
||||
// secrets_detected: split by severity — nuclear is rare but very bad.
|
||||
m.secretsDetected.With(prometheus.Labels{"severity": "normal"}).Set(float64(mockmetrics.RandCount(r, 2_100, 0.25)))
|
||||
m.secretsDetected.With(prometheus.Labels{"severity": "critical"}).Set(float64(mockmetrics.RandCount(r, 750, 0.30)))
|
||||
m.secretsDetected.With(prometheus.Labels{"severity": "nuclear"}).Set(float64(mockmetrics.RandCount(r, 150, 0.35)))
|
||||
m.setCounts(m.secretsDetected, "severity", []countBucket{
|
||||
{label: "normal", mean: 2_100, relStdDev: 0.25},
|
||||
{label: "critical", mean: 750, relStdDev: 0.30},
|
||||
{label: "nuclear", mean: 150, relStdDev: 0.35},
|
||||
})
|
||||
|
||||
// quick_sync_meetings: split by how "quick" they actually were.
|
||||
m.quickSyncMeetings.With(prometheus.Labels{"duration": "10m_to_30m"}).Set(float64(mockmetrics.RandCount(r, 33_000, 0.15)))
|
||||
m.quickSyncMeetings.With(prometheus.Labels{"duration": "30m_to_1h"}).Set(float64(mockmetrics.RandCount(r, 21_000, 0.15)))
|
||||
m.quickSyncMeetings.With(prometheus.Labels{"duration": "1h_plus"}).Set(float64(mockmetrics.RandCount(r, 6_000, 0.25)))
|
||||
m.setCounts(m.quickSyncMeetings, "duration", []countBucket{
|
||||
{label: "10m_to_30m", mean: 33_000, relStdDev: 0.15},
|
||||
{label: "30m_to_1h", mean: 21_000, relStdDev: 0.15},
|
||||
{label: "1h_plus", mean: 6_000, relStdDev: 0.25},
|
||||
})
|
||||
|
||||
// --- Continuous drifting metrics ---
|
||||
|
||||
|
|
|
|||
303
datagen/internal/gen/manager_test.go
Normal file
303
datagen/internal/gen/manager_test.go
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
package gen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/oxynote/oxynote/datagen/internal/mockmetrics"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/metricutil"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
func Test_newContinuous(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
Params mockmetrics.GaugeParams
|
||||
Result float64
|
||||
}{
|
||||
"Start within the bounds is kept": {
|
||||
Params: mockmetrics.GaugeParams{Min: 0, Max: 100, Start: 40},
|
||||
Result: 40,
|
||||
},
|
||||
"Start below the lower bound is raised": {
|
||||
Params: mockmetrics.GaugeParams{Min: 10, Max: 100, Start: 5},
|
||||
Result: 10,
|
||||
},
|
||||
"Start above the upper bound is lowered": {
|
||||
Params: mockmetrics.GaugeParams{Min: 0, Max: 50, Start: 90},
|
||||
Result: 50,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := newContinuous(c.Params)
|
||||
require.NotNil(t, m)
|
||||
|
||||
assert.InDelta(t, c.Result, m.value, 1e-9)
|
||||
assert.Equal(t, c.Params, m.p)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_continuousMetric_tick(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
Params mockmetrics.GaugeParams
|
||||
|
||||
// Result is the value the single tick must produce exactly, used
|
||||
// where the parameters leave no randomness in play.
|
||||
Result *float64
|
||||
|
||||
// Ticks is how many times to advance when the case asserts over a
|
||||
// run rather than a single step.
|
||||
Ticks int
|
||||
}{
|
||||
"Static parameters hold the value still": {
|
||||
Params: mockmetrics.GaugeParams{Min: 0, Max: 100, Start: 50},
|
||||
Result: new(50.0),
|
||||
},
|
||||
"Drift moves the value by a fixed step": {
|
||||
Params: mockmetrics.GaugeParams{Min: 0, Max: 100, Start: 50, DriftPerStep: 2},
|
||||
Result: new(52.0),
|
||||
},
|
||||
"Mean reversion pulls the value toward the target": {
|
||||
Params: mockmetrics.GaugeParams{Min: 0, Max: 100, Start: 50, Target: 100, MeanReversion: 0.5},
|
||||
Result: new(75.0),
|
||||
},
|
||||
"Drift is held at the upper bound": {
|
||||
Params: mockmetrics.GaugeParams{Min: 0, Max: 50, Start: 50, DriftPerStep: 10},
|
||||
Result: new(50.0),
|
||||
},
|
||||
"Drift is held at the lower bound": {
|
||||
Params: mockmetrics.GaugeParams{Min: 10, Max: 100, Start: 10, DriftPerStep: -10},
|
||||
Result: new(10.0),
|
||||
},
|
||||
"Noise keeps the value inside the bounds": {
|
||||
Params: mockmetrics.GaugeParams{Min: 0, Max: 100, Start: 50, NoiseStdDev: 40},
|
||||
Ticks: 1_000,
|
||||
},
|
||||
"A certain spike keeps the value inside the bounds": {
|
||||
Params: mockmetrics.GaugeParams{Min: 0, Max: 100, Start: 50, SpikeChance: 1, SpikeStdDev: 80},
|
||||
Ticks: 1_000,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
m = newContinuous(c.Params)
|
||||
r = mockmetrics.NewRand(1)
|
||||
)
|
||||
|
||||
if c.Result != nil {
|
||||
v := m.tick(r)
|
||||
|
||||
assert.InDelta(t, *c.Result, v, 1e-9)
|
||||
|
||||
// the returned value is the state the next tick builds on.
|
||||
assert.InDelta(t, v, m.value, 1e-9)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for range c.Ticks {
|
||||
v := m.tick(r)
|
||||
|
||||
assert.GreaterOrEqual(t, v, c.Params.Min)
|
||||
assert.LessOrEqual(t, v, c.Params.Max)
|
||||
assert.InDelta(t, v, m.value, 1e-9)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NewManager(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
log := slog.New(slog.DiscardHandler)
|
||||
|
||||
m := NewManager(newFactory(), log)
|
||||
require.NotNil(t, m)
|
||||
|
||||
assert.NotNil(t, m.r)
|
||||
assert.Equal(t, log, m.log)
|
||||
assert.Equal(t, _genInterval, m.interval)
|
||||
|
||||
// every drifting state and every gauge has to be wired, which the
|
||||
// compiler cannot check for a struct built field by field.
|
||||
v := reflect.ValueOf(*m)
|
||||
|
||||
for i := range v.NumField() {
|
||||
name := v.Type().Field(i).Name
|
||||
|
||||
switch v.Type().Field(i).Type.String() {
|
||||
case "*gen.continuousMetric", "metricutil.GaugeVec":
|
||||
assert.False(t, v.Field(i).IsNil(), name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Manager_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fc := newFactory()
|
||||
|
||||
m := NewManager(fc, slog.New(slog.DiscardHandler))
|
||||
m.interval = time.Hour
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(stopCh)
|
||||
|
||||
m.Run(ctx)
|
||||
}()
|
||||
|
||||
// the first generation is immediate, so the gauges carry values well
|
||||
// before the hour-long interval could elapse.
|
||||
require.Eventually(t, func() bool {
|
||||
ff, err := fc.Gather()
|
||||
|
||||
return err == nil && len(ff) > 0
|
||||
}, 5*time.Second, 10*time.Millisecond)
|
||||
|
||||
cancel()
|
||||
|
||||
<-stopCh
|
||||
}
|
||||
|
||||
func Test_Manager_setCounts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fc := newFactory()
|
||||
vec := fc.NewGaugeVec(metricutil.Options{Name: "test_counts", Help: "Tracks nothing."}, []string{"kind"})
|
||||
|
||||
m := &Manager{r: mockmetrics.NewRand(1)}
|
||||
m.setCounts(vec, "kind", []countBucket{
|
||||
{label: "first", mean: 1_000, relStdDev: 0.1},
|
||||
{label: "second", mean: 2_000, relStdDev: 0.1},
|
||||
})
|
||||
|
||||
values := gathered(t, fc, "engineering_test_counts")
|
||||
require.Len(t, values, 2)
|
||||
|
||||
assert.InDelta(t, 1_000, values["first"], 500)
|
||||
assert.InDelta(t, 2_000, values["second"], 1_000)
|
||||
}
|
||||
|
||||
func Test_Manager_generateData(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fc := newFactory()
|
||||
|
||||
m := NewManager(fc, slog.New(slog.DiscardHandler))
|
||||
m.generateData()
|
||||
|
||||
// every declared gauge has to report at least one labelled value.
|
||||
cc := map[string]int{
|
||||
"engineering_meetings_could_be_doc_count": 4,
|
||||
"engineering_circle_back_count": 3,
|
||||
"engineering_friday_deploys_count": 2,
|
||||
"engineering_feature_flag_graveyard_size": 3,
|
||||
"engineering_pizza_fridays_next_30_days": 3,
|
||||
"engineering_deploy_confidence_index": 3,
|
||||
"engineering_ship_it_approvals_after_5pm_count": 3,
|
||||
"engineering_prs_merged_friday_afternoon_count": 3,
|
||||
"engineering_temporary_fix_age_days": 4,
|
||||
"engineering_stale_branches_total": 3,
|
||||
"engineering_pr_time_to_first_review_minutes": 1,
|
||||
"engineering_hotfixes_per_release": 3,
|
||||
"engineering_secrets_detected_commits_count": 3,
|
||||
"engineering_works_on_my_machine_rate": 3,
|
||||
"engineering_quick_sync_meetings_same_day_count": 3,
|
||||
}
|
||||
|
||||
// these subtests read the registry the rest of this function goes on to
|
||||
// write to, so they stay sequential rather than deferring past it.
|
||||
for name, count := range cc {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Len(t, gathered(t, fc, name), count)
|
||||
})
|
||||
}
|
||||
|
||||
// friday deploys are derived rather than sampled, so successes can
|
||||
// never outnumber attempts.
|
||||
deploys := gathered(t, fc, "engineering_friday_deploys_count")
|
||||
assert.LessOrEqual(t, deploys["successful"], deploys["attempted"])
|
||||
|
||||
// a second pass moves the drifting gauges on from where they stopped
|
||||
// rather than restarting them.
|
||||
before := gathered(t, fc, "engineering_pr_time_to_first_review_minutes")
|
||||
|
||||
m.generateData()
|
||||
|
||||
after := gathered(t, fc, "engineering_pr_time_to_first_review_minutes")
|
||||
|
||||
assert.NotEqual(t, before, after)
|
||||
}
|
||||
|
||||
// newFactory creates a metrics factory backed by a registry of its own, so
|
||||
// parallel tests never collide over metric names.
|
||||
func newFactory() metricutil.Factory {
|
||||
return metricutil.NewFactory(
|
||||
"engineering",
|
||||
prometheus.NewRegistry(),
|
||||
metricutil.WithCustomHost("demo"),
|
||||
)
|
||||
}
|
||||
|
||||
// gathered collects the current values of one metric family, keyed by the
|
||||
// value of the single label that splits it (or the empty string when the
|
||||
// family carries no label of its own).
|
||||
func gathered(t *testing.T, fc metricutil.Factory, name string) map[string]float64 {
|
||||
t.Helper()
|
||||
|
||||
ff, err := fc.Gather()
|
||||
require.NoError(t, err)
|
||||
|
||||
out := map[string]float64{}
|
||||
|
||||
for _, f := range ff {
|
||||
if f.GetName() != name {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, m := range f.GetMetric() {
|
||||
out[labelValue(m)] = m.GetGauge().GetValue()
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// labelValue returns the value of the metric's own label, skipping the host
|
||||
// label the factory attaches to everything.
|
||||
func labelValue(m *dto.Metric) string {
|
||||
for _, l := range m.GetLabel() {
|
||||
if l.GetName() != "host" {
|
||||
return l.GetValue()
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// Package mariademo fills the demo MariaDB data source with generated
|
||||
// time-series rows.
|
||||
package mariademo
|
||||
|
||||
import (
|
||||
|
|
@ -8,26 +10,45 @@ import (
|
|||
"math/rand"
|
||||
"time"
|
||||
|
||||
// the mysql driver registers itself with database/sql on import.
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/oxynote/oxynote/datagen/internal/demodata"
|
||||
"github.com/oxynote/oxynote/datagen/internal/mockmetrics"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/timeutil"
|
||||
)
|
||||
|
||||
// _backfillDays defines how much history an empty database is filled with.
|
||||
const _backfillDays = 30
|
||||
|
||||
// _connMaxLifetime bounds how long the single pooled connection is reused.
|
||||
const _connMaxLifetime = 30 * time.Second
|
||||
|
||||
// Generator inserts demo time-series data into MariaDB.
|
||||
type Generator struct {
|
||||
dsn string
|
||||
r *rand.Rand
|
||||
// log is the structured logger.
|
||||
log *slog.Logger
|
||||
|
||||
// dsn addresses the database to fill.
|
||||
dsn string
|
||||
|
||||
// r is the random source used for all row generation.
|
||||
r *rand.Rand
|
||||
|
||||
// interval defines how often a new tick of rows is appended.
|
||||
interval time.Duration
|
||||
|
||||
// backfillDays defines how much history an empty database is filled with.
|
||||
backfillDays int
|
||||
}
|
||||
|
||||
// NewGenerator creates a new demo data generator for MariaDB.
|
||||
// NewGenerator creates a fresh instance of Generator.
|
||||
func NewGenerator(dsn string, log *slog.Logger) *Generator {
|
||||
return &Generator{
|
||||
dsn: dsn,
|
||||
r: mockmetrics.NewRand(time.Now().UnixNano()),
|
||||
log: log,
|
||||
log: log,
|
||||
dsn: dsn,
|
||||
r: mockmetrics.NewRand(timeutil.Now().UnixNano()),
|
||||
interval: demodata.TickInterval,
|
||||
backfillDays: _backfillDays,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -38,13 +59,13 @@ func (g *Generator) Run(ctx context.Context) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("mariademo: open: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
defer db.Close() //nolint:errcheck // error provides no meaningful info
|
||||
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
db.SetConnMaxLifetime(30 * time.Second)
|
||||
db.SetConnMaxLifetime(_connMaxLifetime)
|
||||
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
if err = db.PingContext(ctx); err != nil {
|
||||
return fmt.Errorf("mariademo: ping: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +75,7 @@ func (g *Generator) Run(ctx context.Context) error {
|
|||
}
|
||||
|
||||
if !backfilled {
|
||||
g.log.Info("mariademo: backfilling historical data", slog.Int("days", _backfillDays))
|
||||
g.log.Info("mariademo: backfilling historical data", slog.Int("days", g.backfillDays))
|
||||
|
||||
if err := g.backfill(ctx, db); err != nil {
|
||||
return fmt.Errorf("mariademo: backfill: %w", err)
|
||||
|
|
@ -63,20 +84,22 @@ func (g *Generator) Run(ctx context.Context) error {
|
|||
g.log.Info("mariademo: backfill complete")
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-time.After(demodata.TickInterval):
|
||||
now := time.Now().UTC()
|
||||
|
||||
if err := g.insertTick(ctx, db, now); err != nil {
|
||||
timeutil.NewPeriodicExec(
|
||||
g.interval,
|
||||
0,
|
||||
func(ctx context.Context) {
|
||||
if err := g.insertTick(ctx, db, timeutil.Now()); err != nil {
|
||||
g.log.Error("mariademo: tick insert failed", slog.String("error", err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
nil,
|
||||
false,
|
||||
).Start(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// needsBackfill returns true if historical data already exists.
|
||||
func (g *Generator) needsBackfill(ctx context.Context, db *sql.DB) (bool, error) {
|
||||
var count int
|
||||
|
||||
|
|
@ -88,11 +111,12 @@ func (g *Generator) needsBackfill(ctx context.Context, db *sql.DB) (bool, error)
|
|||
return count > 0, nil
|
||||
}
|
||||
|
||||
// backfill generates backfillDays worth of historical data.
|
||||
func (g *Generator) backfill(ctx context.Context, db *sql.DB) error {
|
||||
now := time.Now().UTC()
|
||||
start := now.AddDate(0, 0, -_backfillDays)
|
||||
now := timeutil.Now()
|
||||
start := now.AddDate(0, 0, -g.backfillDays)
|
||||
|
||||
for t := start; t.Before(now); t = t.Add(demodata.TickInterval) {
|
||||
for t := start; t.Before(now); t = t.Add(g.interval) {
|
||||
if err := g.insertTick(ctx, db, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -101,6 +125,7 @@ func (g *Generator) backfill(ctx context.Context, db *sql.DB) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// insertTick generates and inserts one tick's worth of data at the given time.
|
||||
func (g *Generator) insertTick(ctx context.Context, db *sql.DB, t time.Time) error {
|
||||
tick := demodata.GenerateTick(g.r, t)
|
||||
|
||||
|
|
@ -108,7 +133,7 @@ func (g *Generator) insertTick(ctx context.Context, db *sql.DB, t time.Time) err
|
|||
if err != nil {
|
||||
return fmt.Errorf("begin tx at %s: %w", t.Format(time.RFC3339), err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
defer tx.Rollback() //nolint:errcheck // error provides no meaningful info
|
||||
|
||||
for _, d := range tick.Deployments {
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
|
|
|
|||
566
datagen/internal/mariademo/mariademo_test.go
Normal file
566
datagen/internal/mariademo/mariademo_test.go
Normal file
|
|
@ -0,0 +1,566 @@
|
|||
package mariademo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
mysqlDriver "github.com/go-sql-driver/mysql"
|
||||
"github.com/orlangure/gnomock"
|
||||
"github.com/oxynote/oxynote/datagen/internal/demodata"
|
||||
"github.com/oxynote/oxynote/datagen/internal/mockmetrics"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
const (
|
||||
// _testDB is the database name used in the throwaway container.
|
||||
_testDB = "testdb"
|
||||
|
||||
// _rootUser is the privileged container user.
|
||||
_rootUser = "root"
|
||||
|
||||
// _rootPass is the privileged container password.
|
||||
_rootPass = "rootpass"
|
||||
)
|
||||
|
||||
// _schema mirrors docker/demo/mariadb/init-demo-db.sql, which is what the
|
||||
// demo data source is really created with.
|
||||
var _schema = []string{
|
||||
`CREATE TABLE deployments (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
time DATETIME NOT NULL,
|
||||
service VARCHAR(255) NOT NULL,
|
||||
environment VARCHAR(255) NOT NULL,
|
||||
duration_seconds DOUBLE NOT NULL,
|
||||
success BOOLEAN NOT NULL,
|
||||
rollback BOOLEAN NOT NULL DEFAULT FALSE
|
||||
)`,
|
||||
`CREATE TABLE incidents (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
time DATETIME NOT NULL,
|
||||
severity VARCHAR(255) NOT NULL,
|
||||
service VARCHAR(255) NOT NULL,
|
||||
time_to_detect_minutes DOUBLE NOT NULL,
|
||||
time_to_resolve_minutes DOUBLE NOT NULL
|
||||
)`,
|
||||
`CREATE TABLE build_metrics (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
time DATETIME NOT NULL,
|
||||
repository VARCHAR(255) NOT NULL,
|
||||
branch VARCHAR(255) NOT NULL,
|
||||
duration_seconds DOUBLE NOT NULL,
|
||||
test_count INT NOT NULL,
|
||||
tests_failed INT NOT NULL,
|
||||
coverage_pct DOUBLE NOT NULL
|
||||
)`,
|
||||
}
|
||||
|
||||
// _addr is the address of the throwaway MariaDB container.
|
||||
var _addr string
|
||||
|
||||
// _tick is a weekday office-hours instant, busy enough that every table gets
|
||||
// rows.
|
||||
var _tick = time.Date(2024, time.January, 9, 11, 0, 0, 0, time.UTC)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// silence the driver's connection noise while the container boots.
|
||||
if err := mysqlDriver.SetLogger(log.New(io.Discard, "", 0)); err != nil {
|
||||
panic("cannot set mysql logger: " + err.Error())
|
||||
}
|
||||
|
||||
container, err := gnomock.StartCustom(
|
||||
"docker.io/library/mariadb:11-noble",
|
||||
gnomock.DefaultTCP(3306),
|
||||
gnomock.WithEnv("MARIADB_ROOT_PASSWORD="+_rootPass),
|
||||
gnomock.WithEnv("MARIADB_DATABASE="+_testDB),
|
||||
gnomock.WithHealthCheck(healthcheck),
|
||||
)
|
||||
if err != nil {
|
||||
panic("cannot set up mariadb: " + err.Error())
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err = gnomock.Stop(container); err != nil {
|
||||
panic("cannot clean up mariadb: " + err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
_addr = container.Host + ":" + strconv.Itoa(container.DefaultPort())
|
||||
|
||||
goleak.VerifyTestMain(m, goleak.IgnoreCurrent())
|
||||
}
|
||||
|
||||
// healthcheck reports whether the container accepts root connections.
|
||||
func healthcheck(ctx context.Context, c *gnomock.Container) error {
|
||||
db, err := sql.Open("mysql", fmt.Sprintf(
|
||||
"%s:%s@tcp(%s)/%s",
|
||||
_rootUser,
|
||||
_rootPass,
|
||||
c.Address(gnomock.DefaultPort),
|
||||
_testDB,
|
||||
))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer db.Close() //nolint:errcheck // error provides no meaningful info
|
||||
|
||||
return db.PingContext(ctx)
|
||||
}
|
||||
|
||||
func Test_NewGenerator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
lg := slog.New(slog.DiscardHandler)
|
||||
|
||||
g := NewGenerator("user:pass@tcp(host)/db", lg)
|
||||
require.NotNil(t, g)
|
||||
|
||||
assert.Equal(t, lg, g.log)
|
||||
assert.Equal(t, "user:pass@tcp(host)/db", g.dsn)
|
||||
assert.NotNil(t, g.r)
|
||||
assert.Equal(t, demodata.TickInterval, g.interval)
|
||||
assert.Equal(t, _backfillDays, g.backfillDays)
|
||||
}
|
||||
|
||||
func Test_Generator_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
// DSN overrides the throwaway database's DSN, for the cases that
|
||||
// must fail before any query runs.
|
||||
DSN string
|
||||
|
||||
// DropTable names a table to remove before the run, so a query
|
||||
// fails against a real connection.
|
||||
DropTable string
|
||||
|
||||
Err error
|
||||
}{
|
||||
"Error returned by sql.Open": {
|
||||
DSN: "not a dsn at all",
|
||||
Err: assert.AnError,
|
||||
},
|
||||
"Error returned by the ping": {
|
||||
DSN: fmt.Sprintf("%s:%s@tcp(127.0.0.1:1)/%s", _rootUser, _rootPass, _testDB),
|
||||
Err: assert.AnError,
|
||||
},
|
||||
"Error returned by needsBackfill": {
|
||||
DropTable: "deployments",
|
||||
Err: assert.AnError,
|
||||
},
|
||||
"Error returned by backfill": {
|
||||
DropTable: "incidents",
|
||||
Err: assert.AnError,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := prepGenerator(t)
|
||||
if c.DSN != "" {
|
||||
g.dsn = c.DSN
|
||||
}
|
||||
|
||||
if c.DropTable != "" {
|
||||
exec(t, g.dsn, "DROP TABLE "+c.DropTable)
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
testutil.AssertEqualError(t, c.Err, g.Run(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// an empty database is backfilled before the tick loop starts.
|
||||
t.Run("Successful run backfills an empty database", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
lg, wr, content := bufferLog()
|
||||
|
||||
g := prepGenerator(t)
|
||||
g.log = lg
|
||||
|
||||
// a wide interval keeps the backfill to a handful of ticks; the
|
||||
// periodic loop then never fires before the context is cancelled.
|
||||
g.interval = 6 * time.Hour
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(stopCh)
|
||||
|
||||
assert.NoError(t, g.Run(ctx))
|
||||
}()
|
||||
|
||||
// cancelling mid-backfill would abort it, so wait for the run to
|
||||
// announce that it finished.
|
||||
require.Eventually(t, func() bool {
|
||||
require.NoError(t, wr.Flush())
|
||||
|
||||
return strings.Contains(content(), "backfill complete")
|
||||
}, 60*time.Second, 50*time.Millisecond)
|
||||
|
||||
assert.Positive(t, count(t, g.dsn, "deployments"))
|
||||
|
||||
cancel()
|
||||
|
||||
<-stopCh
|
||||
})
|
||||
|
||||
// a populated database skips the backfill and goes straight to
|
||||
// appending a tick per interval.
|
||||
t.Run("Successful run appends to a populated database", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
lg, wr, content := bufferLog()
|
||||
|
||||
g := prepGenerator(t)
|
||||
g.log = lg
|
||||
|
||||
db := open(t, g.dsn)
|
||||
require.NoError(t, g.insertTick(context.Background(), db, _tick))
|
||||
|
||||
seeded := count(t, g.dsn, "deployments")
|
||||
require.Positive(t, seeded)
|
||||
|
||||
g.interval = 10 * time.Millisecond
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(stopCh)
|
||||
|
||||
assert.NoError(t, g.Run(ctx))
|
||||
}()
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
return count(t, g.dsn, "deployments") > seeded
|
||||
}, 60*time.Second, 50*time.Millisecond)
|
||||
|
||||
cancel()
|
||||
|
||||
<-stopCh
|
||||
|
||||
require.NoError(t, wr.Flush())
|
||||
assert.NotContains(t, content(), "backfilling historical data")
|
||||
})
|
||||
|
||||
// a failure inside the tick loop is logged and the loop carries on.
|
||||
t.Run("Tick insert failure is logged", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
lg, wr, content := bufferLog()
|
||||
|
||||
g := prepGenerator(t)
|
||||
g.log = lg
|
||||
|
||||
require.NoError(t, g.insertTick(context.Background(), open(t, g.dsn), _tick))
|
||||
|
||||
seeded := count(t, g.dsn, "deployments")
|
||||
require.Positive(t, seeded)
|
||||
|
||||
g.interval = 10 * time.Millisecond
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(stopCh)
|
||||
|
||||
assert.NoError(t, g.Run(ctx))
|
||||
}()
|
||||
|
||||
// the table can only be pulled out from under the loop once the
|
||||
// loop is running, which its first successful insert proves.
|
||||
require.Eventually(t, func() bool {
|
||||
return count(t, g.dsn, "deployments") > seeded
|
||||
}, 60*time.Second, 50*time.Millisecond)
|
||||
|
||||
exec(t, g.dsn, "DROP TABLE deployments")
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
require.NoError(t, wr.Flush())
|
||||
|
||||
return strings.Contains(content(), "tick insert failed")
|
||||
}, 60*time.Second, 50*time.Millisecond)
|
||||
|
||||
cancel()
|
||||
|
||||
<-stopCh
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Generator_needsBackfill(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
// Seed indicates whether rows are inserted before the check.
|
||||
Seed bool
|
||||
|
||||
// DropTable removes the table so the query itself fails.
|
||||
DropTable bool
|
||||
|
||||
Result bool
|
||||
Err error
|
||||
}{
|
||||
"Empty database still needs a backfill": {},
|
||||
"Populated database does not": {
|
||||
Seed: true,
|
||||
Result: true,
|
||||
},
|
||||
"Error returned by the query": {
|
||||
DropTable: true,
|
||||
Err: assert.AnError,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := prepGenerator(t)
|
||||
db := open(t, g.dsn)
|
||||
|
||||
if c.Seed {
|
||||
require.NoError(t, g.insertTick(context.Background(), db, _tick))
|
||||
}
|
||||
|
||||
if c.DropTable {
|
||||
exec(t, g.dsn, "DROP TABLE deployments")
|
||||
}
|
||||
|
||||
res, err := g.needsBackfill(context.Background(), db)
|
||||
testutil.AssertEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, c.Result, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Generator_backfill(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
// DropTable removes a table so an insert inside the loop fails.
|
||||
DropTable string
|
||||
|
||||
Err error
|
||||
}{
|
||||
"Successful backfill": {},
|
||||
"Error returned by insertTick": {
|
||||
DropTable: "deployments",
|
||||
Err: assert.AnError,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := prepGenerator(t)
|
||||
|
||||
if c.DropTable != "" {
|
||||
exec(t, g.dsn, "DROP TABLE "+c.DropTable)
|
||||
}
|
||||
|
||||
err := g.backfill(context.Background(), open(t, g.dsn))
|
||||
testutil.AssertEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// one day of ticks covers a stretch of demo history, so every
|
||||
// table has to have been written to.
|
||||
assert.Positive(t, count(t, g.dsn, "deployments"))
|
||||
assert.Positive(t, count(t, g.dsn, "incidents"))
|
||||
assert.Positive(t, count(t, g.dsn, "build_metrics"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Generator_insertTick(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
// DropTable removes a table so one of the inserts fails.
|
||||
DropTable string
|
||||
|
||||
// CloseDB indicates the pool should be closed first, so the
|
||||
// transaction cannot even begin.
|
||||
CloseDB bool
|
||||
|
||||
Err error
|
||||
}{
|
||||
"Successful insert": {},
|
||||
"Error returned by the deployment insert": {
|
||||
DropTable: "deployments",
|
||||
Err: assert.AnError,
|
||||
},
|
||||
"Error returned by the incident insert": {
|
||||
DropTable: "incidents",
|
||||
Err: assert.AnError,
|
||||
},
|
||||
"Error returned by the build insert": {
|
||||
DropTable: "build_metrics",
|
||||
Err: assert.AnError,
|
||||
},
|
||||
"Error returned by BeginTx": {
|
||||
CloseDB: true,
|
||||
Err: assert.AnError,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := prepGenerator(t)
|
||||
db := open(t, g.dsn)
|
||||
|
||||
if c.DropTable != "" {
|
||||
exec(t, g.dsn, "DROP TABLE "+c.DropTable)
|
||||
}
|
||||
|
||||
if c.CloseDB {
|
||||
require.NoError(t, db.Close())
|
||||
}
|
||||
|
||||
err := g.insertTick(context.Background(), db, _tick)
|
||||
testutil.AssertEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Positive(t, count(t, g.dsn, "deployments"))
|
||||
assert.Positive(t, count(t, g.dsn, "incidents"))
|
||||
assert.Positive(t, count(t, g.dsn, "build_metrics"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// prepGenerator gives a test its own randomly named database in the shared
|
||||
// container, already carrying the demo schema.
|
||||
func prepGenerator(t *testing.T) *Generator {
|
||||
t.Helper()
|
||||
|
||||
name := "demo_" + strconv.FormatInt(time.Now().UnixNano(), 36) + strconv.Itoa(len(t.Name()))
|
||||
|
||||
exec(t, dsn(_testDB), "CREATE DATABASE "+name)
|
||||
|
||||
for _, q := range _schema {
|
||||
exec(t, dsn(name), q)
|
||||
}
|
||||
|
||||
g := NewGenerator(dsn(name), slog.New(slog.DiscardHandler))
|
||||
|
||||
// a single day keeps the backfill loop to a few hundred ticks instead
|
||||
// of the month the generator runs with in production.
|
||||
g.backfillDays = 1
|
||||
|
||||
// a pinned seed makes the first tick at _tick fill every table, so no
|
||||
// test depends on a sparse table happening to draw a row.
|
||||
g.r = mockmetrics.NewRand(_fullDrawSeed)
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
// dsn builds a connection string for one database in the container.
|
||||
func dsn(db string) string {
|
||||
return fmt.Sprintf("%s:%s@tcp(%s)/%s?parseTime=true", _rootUser, _rootPass, _addr, db)
|
||||
}
|
||||
|
||||
// open opens a pool that is closed when the test ends.
|
||||
func open(t *testing.T, dsn string) *sql.DB {
|
||||
t.Helper()
|
||||
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
// a case that closed the pool itself leaves nothing to do here.
|
||||
db.Close() //nolint:errcheck,gosec // the pool may already be closed
|
||||
})
|
||||
|
||||
require.NoError(t, db.PingContext(context.Background()))
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// exec runs a single statement against the given database.
|
||||
func exec(t *testing.T, dsn, query string) {
|
||||
t.Helper()
|
||||
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer db.Close() //nolint:errcheck // error provides no meaningful info
|
||||
|
||||
_, err = db.ExecContext(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// count returns the number of rows in a table over a pool of its own, so the
|
||||
// polling callers cannot pile up connections.
|
||||
func count(t *testing.T, dsn, table string) int {
|
||||
t.Helper()
|
||||
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer db.Close() //nolint:errcheck // error provides no meaningful info
|
||||
|
||||
var n int
|
||||
|
||||
require.NoError(t, db.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM "+table).Scan(&n))
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// bufferLog creates a logger writing into a buffer whose contents the caller
|
||||
// can read back once flushed.
|
||||
func bufferLog() (*slog.Logger, *testutil.Writer, func() string) {
|
||||
wr, buf := testutil.NewBuffer()
|
||||
|
||||
return slog.New(slog.NewJSONHandler(wr, nil)), wr, buf.String
|
||||
}
|
||||
|
||||
// _fullDrawSeed is a seed whose first draw at _tick fills every table, so a
|
||||
// test can make any one of the three inserts fail.
|
||||
var _fullDrawSeed = fullDraw()
|
||||
|
||||
// fullDraw searches for that seed.
|
||||
func fullDraw() int64 {
|
||||
for seed := int64(1); ; seed++ {
|
||||
tick := demodata.GenerateTick(mockmetrics.NewRand(seed), _tick)
|
||||
if len(tick.Deployments) > 0 && len(tick.Incidents) > 0 && len(tick.BuildMetrics) > 0 {
|
||||
return seed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// Package mockmetrics implements the random distributions the demo metrics
|
||||
// and demo database rows are sampled from.
|
||||
package mockmetrics
|
||||
|
||||
import (
|
||||
|
|
@ -10,6 +12,7 @@ func Normal(r *rand.Rand, mean, stddev float64) float64 {
|
|||
u1 := r.Float64()
|
||||
u2 := r.Float64()
|
||||
z := math.Sqrt(-2.0*math.Log(u1)) * math.Cos(2.0*math.Pi*u2)
|
||||
|
||||
return mean + stddev*z
|
||||
}
|
||||
|
||||
|
|
@ -26,29 +29,36 @@ func Clamp(x, lo, hi float64) float64 {
|
|||
return x
|
||||
}
|
||||
|
||||
// _poissonNormalLambda is the lambda above which Poisson switches from
|
||||
// Knuth's algorithm to a Normal approximation.
|
||||
const _poissonNormalLambda = 50
|
||||
|
||||
// Poisson returns a Poisson-distributed sample with the given lambda.
|
||||
// Knuth's algorithm is used for small lambda; for lambda > 50 a Normal
|
||||
// approximation (mean=lambda, stddev=sqrt(lambda)) is used instead, which
|
||||
// is accurate and avoids the O(lambda) loop cost at large scale.
|
||||
// Knuth's algorithm is used for small lambda; above _poissonNormalLambda a
|
||||
// Normal approximation (mean=lambda, stddev=sqrt(lambda)) is used instead,
|
||||
// which is accurate and avoids the O(lambda) loop cost at large scale.
|
||||
func Poisson(r *rand.Rand, lambda float64) int {
|
||||
if lambda <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
if lambda > 50 {
|
||||
if lambda > _poissonNormalLambda {
|
||||
v := int(math.Round(Normal(r, lambda, math.Sqrt(lambda))))
|
||||
if v < 0 {
|
||||
// NOCOV: reaching this needs a sample more than sqrt(lambda)
|
||||
// deviations below the mean, which past lambda 50 is a
|
||||
// once-in-a-trillion draw.
|
||||
return 0
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
L := math.Exp(-lambda)
|
||||
limit := math.Exp(-lambda)
|
||||
k := 0
|
||||
p := 1.0
|
||||
|
||||
for p > L {
|
||||
for p > limit {
|
||||
k++
|
||||
p *= r.Float64()
|
||||
}
|
||||
|
|
@ -71,5 +81,6 @@ func RandCount(r *rand.Rand, mean, relStdDev float64) int {
|
|||
|
||||
// NewRand returns a new rand seeded with seed.
|
||||
func NewRand(seed int64) *rand.Rand {
|
||||
//nolint:gosec // demo data wants a cheap reproducible source, not a cryptographic one
|
||||
return rand.New(rand.NewSource(seed))
|
||||
}
|
||||
|
|
|
|||
250
datagen/internal/mockmetrics/rand_test.go
Normal file
250
datagen/internal/mockmetrics/rand_test.go
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
package mockmetrics
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
func Test_Normal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
Mean float64
|
||||
StdDev float64
|
||||
}{
|
||||
"Zero deviation always returns the mean": {
|
||||
Mean: 42,
|
||||
},
|
||||
"Non-zero deviation spreads around the mean": {
|
||||
Mean: 100,
|
||||
StdDev: 15,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRand(1)
|
||||
|
||||
// a zero deviation collapses the distribution onto the mean,
|
||||
// which is the only exact assertion available here.
|
||||
if c.StdDev == 0 {
|
||||
assert.InDelta(t, c.Mean, Normal(r, c.Mean, c.StdDev), 1e-9)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise the sample mean of a large draw must land near the
|
||||
// requested mean, and the samples must not all be identical.
|
||||
var (
|
||||
sum float64
|
||||
first = Normal(r, c.Mean, c.StdDev)
|
||||
same = true
|
||||
)
|
||||
|
||||
sum += first
|
||||
|
||||
for range 9_999 {
|
||||
v := Normal(r, c.Mean, c.StdDev)
|
||||
if v != first {
|
||||
same = false
|
||||
}
|
||||
|
||||
sum += v
|
||||
}
|
||||
|
||||
assert.False(t, same)
|
||||
assert.InDelta(t, c.Mean, sum/10_000, c.StdDev)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Clamp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
X float64
|
||||
Lo float64
|
||||
Hi float64
|
||||
Result float64
|
||||
}{
|
||||
"Below the lower bound": {
|
||||
X: -5,
|
||||
Lo: 0,
|
||||
Hi: 10,
|
||||
Result: 0,
|
||||
},
|
||||
"Above the upper bound": {
|
||||
X: 15,
|
||||
Lo: 0,
|
||||
Hi: 10,
|
||||
Result: 10,
|
||||
},
|
||||
"Within the bounds": {
|
||||
X: 5,
|
||||
Lo: 0,
|
||||
Hi: 10,
|
||||
Result: 5,
|
||||
},
|
||||
"Exactly on the lower bound": {
|
||||
X: 0,
|
||||
Lo: 0,
|
||||
Hi: 10,
|
||||
Result: 0,
|
||||
},
|
||||
"Exactly on the upper bound": {
|
||||
X: 10,
|
||||
Lo: 0,
|
||||
Hi: 10,
|
||||
Result: 10,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.InDelta(t, c.Result, Clamp(c.X, c.Lo, c.Hi), 1e-9)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Poisson(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
Lambda float64
|
||||
|
||||
// Exact holds the single value every draw must produce, used for the
|
||||
// degenerate lambdas that cannot vary.
|
||||
Exact *int
|
||||
|
||||
// Draws is how many samples the statistical assertions run over.
|
||||
Draws int
|
||||
}{
|
||||
"Zero lambda never produces an event": {
|
||||
Lambda: 0,
|
||||
Exact: new(0),
|
||||
},
|
||||
"Negative lambda never produces an event": {
|
||||
Lambda: -1,
|
||||
Exact: new(0),
|
||||
},
|
||||
"Small lambda goes through Knuth's algorithm": {
|
||||
Lambda: 5,
|
||||
Draws: 10_000,
|
||||
},
|
||||
"Lambda on the switchover boundary still uses Knuth": {
|
||||
Lambda: _poissonNormalLambda,
|
||||
Draws: 10_000,
|
||||
},
|
||||
"Large lambda goes through the normal approximation": {
|
||||
Lambda: 5_000,
|
||||
Draws: 10_000,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRand(1)
|
||||
|
||||
if c.Exact != nil {
|
||||
assert.Equal(t, *c.Exact, Poisson(r, c.Lambda))
|
||||
return
|
||||
}
|
||||
|
||||
var sum int
|
||||
|
||||
for range c.Draws {
|
||||
v := Poisson(r, c.Lambda)
|
||||
require.GreaterOrEqual(t, v, 0)
|
||||
|
||||
sum += v
|
||||
}
|
||||
|
||||
// the mean of a Poisson draw is lambda itself; allow a generous
|
||||
// band so the assertion never races the sampler.
|
||||
assert.InDelta(t, c.Lambda, float64(sum)/float64(c.Draws), math.Sqrt(c.Lambda))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_RandCount(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
Mean float64
|
||||
RelStdDev float64
|
||||
|
||||
// Exact holds the value every draw must produce when the parameters
|
||||
// leave no room for variation.
|
||||
Exact *int
|
||||
}{
|
||||
"Zero deviation returns the rounded mean": {
|
||||
Mean: 1_000,
|
||||
Exact: new(1_000),
|
||||
},
|
||||
"Negative mean is clamped to zero": {
|
||||
Mean: -50,
|
||||
Exact: new(0),
|
||||
},
|
||||
"Spread mean stays non-negative": {
|
||||
Mean: 500,
|
||||
RelStdDev: 0.2,
|
||||
},
|
||||
"Deviation wide enough to undershoot zero is clamped": {
|
||||
Mean: 10,
|
||||
RelStdDev: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRand(1)
|
||||
|
||||
if c.Exact != nil {
|
||||
assert.Equal(t, *c.Exact, RandCount(r, c.Mean, c.RelStdDev))
|
||||
return
|
||||
}
|
||||
|
||||
var sum int
|
||||
|
||||
for range 10_000 {
|
||||
v := RandCount(r, c.Mean, c.RelStdDev)
|
||||
require.GreaterOrEqual(t, v, 0)
|
||||
|
||||
sum += v
|
||||
}
|
||||
|
||||
assert.InDelta(t, c.Mean, float64(sum)/10_000, c.Mean*c.RelStdDev)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NewRand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// the same seed replays the same sequence.
|
||||
a, b := NewRand(7), NewRand(7)
|
||||
require.NotNil(t, a)
|
||||
require.NotNil(t, b)
|
||||
|
||||
for range 100 {
|
||||
assert.InDelta(t, a.Float64(), b.Float64(), 1e-12)
|
||||
}
|
||||
|
||||
// a different seed does not.
|
||||
assert.NotEqual(t, NewRand(7).Float64(), NewRand(8).Float64())
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
// Package pgdemo fills the demo PostgreSQL data source with generated
|
||||
// time-series rows.
|
||||
package pgdemo
|
||||
|
||||
import (
|
||||
|
|
@ -10,23 +12,38 @@ import (
|
|||
"github.com/jackc/pgx/v5"
|
||||
"github.com/oxynote/oxynote/datagen/internal/demodata"
|
||||
"github.com/oxynote/oxynote/datagen/internal/mockmetrics"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/timeutil"
|
||||
)
|
||||
|
||||
// _backfillDays defines how much history an empty database is filled with.
|
||||
const _backfillDays = 30
|
||||
|
||||
// Generator inserts demo time-series data into PostgreSQL.
|
||||
type Generator struct {
|
||||
dsn string
|
||||
r *rand.Rand
|
||||
// log is the structured logger.
|
||||
log *slog.Logger
|
||||
|
||||
// dsn addresses the database to fill.
|
||||
dsn string
|
||||
|
||||
// r is the random source used for all row generation.
|
||||
r *rand.Rand
|
||||
|
||||
// interval defines how often a new tick of rows is appended.
|
||||
interval time.Duration
|
||||
|
||||
// backfillDays defines how much history an empty database is filled with.
|
||||
backfillDays int
|
||||
}
|
||||
|
||||
// NewGenerator creates a new demo data generator.
|
||||
// NewGenerator creates a fresh instance of Generator.
|
||||
func NewGenerator(dsn string, log *slog.Logger) *Generator {
|
||||
return &Generator{
|
||||
dsn: dsn,
|
||||
r: mockmetrics.NewRand(time.Now().UnixNano()),
|
||||
log: log,
|
||||
log: log,
|
||||
dsn: dsn,
|
||||
r: mockmetrics.NewRand(timeutil.Now().UnixNano()),
|
||||
interval: demodata.TickInterval,
|
||||
backfillDays: _backfillDays,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +54,7 @@ func (g *Generator) Run(ctx context.Context) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("pgdemo: connect: %w", err)
|
||||
}
|
||||
defer conn.Close(ctx)
|
||||
defer conn.Close(ctx) //nolint:errcheck // error provides no meaningful info
|
||||
|
||||
backfilled, err := g.needsBackfill(ctx, conn)
|
||||
if err != nil {
|
||||
|
|
@ -45,7 +62,7 @@ func (g *Generator) Run(ctx context.Context) error {
|
|||
}
|
||||
|
||||
if !backfilled {
|
||||
g.log.Info("pgdemo: backfilling historical data", slog.Int("days", _backfillDays))
|
||||
g.log.Info("pgdemo: backfilling historical data", slog.Int("days", g.backfillDays))
|
||||
|
||||
if err := g.backfill(ctx, conn); err != nil {
|
||||
return fmt.Errorf("pgdemo: backfill: %w", err)
|
||||
|
|
@ -54,18 +71,19 @@ func (g *Generator) Run(ctx context.Context) error {
|
|||
g.log.Info("pgdemo: backfill complete")
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-time.After(demodata.TickInterval):
|
||||
now := time.Now().UTC()
|
||||
|
||||
if err := g.insertTick(ctx, conn, now); err != nil {
|
||||
timeutil.NewPeriodicExec(
|
||||
g.interval,
|
||||
0,
|
||||
func(ctx context.Context) {
|
||||
if err := g.insertTick(ctx, conn, timeutil.Now()); err != nil {
|
||||
g.log.Error("pgdemo: tick insert failed", slog.String("error", err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
nil,
|
||||
false,
|
||||
).Start(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// needsBackfill returns true if historical data already exists.
|
||||
|
|
@ -80,12 +98,12 @@ func (g *Generator) needsBackfill(ctx context.Context, conn *pgx.Conn) (bool, er
|
|||
return count > 0, nil
|
||||
}
|
||||
|
||||
// backfill generates _backfillDays worth of historical data.
|
||||
// backfill generates backfillDays worth of historical data.
|
||||
func (g *Generator) backfill(ctx context.Context, conn *pgx.Conn) error {
|
||||
now := time.Now().UTC()
|
||||
start := now.AddDate(0, 0, -_backfillDays)
|
||||
now := timeutil.Now()
|
||||
start := now.AddDate(0, 0, -g.backfillDays)
|
||||
|
||||
for t := start; t.Before(now); t = t.Add(demodata.TickInterval) {
|
||||
for t := start; t.Before(now); t = t.Add(g.interval) {
|
||||
if err := g.insertTick(ctx, conn, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -125,7 +143,7 @@ func (g *Generator) insertTick(ctx context.Context, conn *pgx.Conn, t time.Time)
|
|||
}
|
||||
|
||||
br := conn.SendBatch(ctx, batch)
|
||||
defer br.Close()
|
||||
defer br.Close() //nolint:errcheck // the exec errors below already report what failed
|
||||
|
||||
for range batch.Len() {
|
||||
if _, err := br.Exec(); err != nil {
|
||||
|
|
|
|||
547
datagen/internal/pgdemo/pgdemo_test.go
Normal file
547
datagen/internal/pgdemo/pgdemo_test.go
Normal file
|
|
@ -0,0 +1,547 @@
|
|||
package pgdemo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/orlangure/gnomock"
|
||||
pgDocker "github.com/orlangure/gnomock/preset/postgres"
|
||||
"github.com/oxynote/oxynote/datagen/internal/demodata"
|
||||
"github.com/oxynote/oxynote/datagen/internal/mockmetrics"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
const (
|
||||
// _testDB is the database name used in the throwaway container.
|
||||
_testDB = "testdb"
|
||||
|
||||
// _pgUser is the privileged container user.
|
||||
_pgUser = "pgtest"
|
||||
|
||||
// _pgPass is the privileged container password.
|
||||
_pgPass = "pgpass"
|
||||
)
|
||||
|
||||
// _schema mirrors docker/demo/postgres/init-demo-db.sh, which is what the
|
||||
// demo data source is really created with.
|
||||
var _schema = []string{
|
||||
`CREATE TABLE deployments (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time TIMESTAMPTZ NOT NULL,
|
||||
service TEXT NOT NULL,
|
||||
environment TEXT NOT NULL,
|
||||
duration_seconds DOUBLE PRECISION NOT NULL,
|
||||
success BOOLEAN NOT NULL,
|
||||
rollback BOOLEAN NOT NULL DEFAULT FALSE
|
||||
)`,
|
||||
`CREATE TABLE incidents (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time TIMESTAMPTZ NOT NULL,
|
||||
severity TEXT NOT NULL,
|
||||
service TEXT NOT NULL,
|
||||
time_to_detect_minutes DOUBLE PRECISION NOT NULL,
|
||||
time_to_resolve_minutes DOUBLE PRECISION NOT NULL
|
||||
)`,
|
||||
`CREATE TABLE build_metrics (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
time TIMESTAMPTZ NOT NULL,
|
||||
repository TEXT NOT NULL,
|
||||
branch TEXT NOT NULL,
|
||||
duration_seconds DOUBLE PRECISION NOT NULL,
|
||||
test_count INTEGER NOT NULL,
|
||||
tests_failed INTEGER NOT NULL,
|
||||
coverage_pct DOUBLE PRECISION NOT NULL
|
||||
)`,
|
||||
}
|
||||
|
||||
// _pgAddr is the address of the throwaway PostgreSQL container.
|
||||
var _pgAddr string
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
container, err := gnomock.Start(pgDocker.Preset(
|
||||
pgDocker.WithUser(_pgUser, _pgPass),
|
||||
pgDocker.WithDatabase(_testDB),
|
||||
))
|
||||
if err != nil {
|
||||
panic("cannot set up postgres: " + err.Error())
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err = gnomock.Stop(container); err != nil {
|
||||
panic("cannot clean up postgres: " + err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
_pgAddr = container.Host + ":" + strconv.Itoa(container.DefaultPort())
|
||||
|
||||
goleak.VerifyTestMain(m, goleak.IgnoreCurrent())
|
||||
}
|
||||
|
||||
func Test_NewGenerator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
log := slog.New(slog.DiscardHandler)
|
||||
|
||||
g := NewGenerator("postgres://test", log)
|
||||
require.NotNil(t, g)
|
||||
|
||||
assert.Equal(t, log, g.log)
|
||||
assert.Equal(t, "postgres://test", g.dsn)
|
||||
assert.NotNil(t, g.r)
|
||||
assert.Equal(t, demodata.TickInterval, g.interval)
|
||||
assert.Equal(t, _backfillDays, g.backfillDays)
|
||||
}
|
||||
|
||||
func Test_Generator_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
// DSN overrides the throwaway database's DSN, for the cases that
|
||||
// must fail before any query runs.
|
||||
DSN string
|
||||
|
||||
// DropTable names a table to remove before the run, so the backfill
|
||||
// check fails against a real connection.
|
||||
DropTable string
|
||||
|
||||
// Err is the expected outcome of the run.
|
||||
Err error
|
||||
}{
|
||||
"Error returned by pgx.Connect": {
|
||||
DSN: "postgres://nobody@127.0.0.1:1/nothing",
|
||||
Err: assert.AnError,
|
||||
},
|
||||
"Error returned by needsBackfill": {
|
||||
DropTable: "deployments",
|
||||
Err: assert.AnError,
|
||||
},
|
||||
"Error returned by backfill": {
|
||||
DropTable: "incidents",
|
||||
Err: assert.AnError,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := prepGenerator(t)
|
||||
if c.DSN != "" {
|
||||
g.dsn = c.DSN
|
||||
}
|
||||
|
||||
if c.DropTable != "" {
|
||||
exec(t, g.dsn, "DROP TABLE "+c.DropTable)
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
testutil.AssertEqualError(t, c.Err, g.Run(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// an empty database is backfilled before the tick loop starts.
|
||||
t.Run("Successful run backfills an empty database", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
log, wr, content := bufferLog()
|
||||
|
||||
g := prepGenerator(t)
|
||||
g.log = log
|
||||
|
||||
// a wide interval keeps the backfill to a handful of ticks; the
|
||||
// periodic loop then never fires before the context is cancelled.
|
||||
g.interval = 6 * time.Hour
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(stopCh)
|
||||
|
||||
assert.NoError(t, g.Run(ctx))
|
||||
}()
|
||||
|
||||
// cancelling mid-backfill would abort it, so wait for the run to
|
||||
// announce that it finished.
|
||||
require.Eventually(t, func() bool {
|
||||
require.NoError(t, wr.Flush())
|
||||
|
||||
return strings.Contains(content(), "backfill complete")
|
||||
}, 60*time.Second, 50*time.Millisecond)
|
||||
|
||||
assert.Positive(t, count(t, g.dsn, "deployments"))
|
||||
|
||||
cancel()
|
||||
|
||||
<-stopCh
|
||||
})
|
||||
|
||||
// a populated database skips the backfill and goes straight to
|
||||
// appending a tick per interval.
|
||||
t.Run("Successful run appends to a populated database", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
log, wr, content := bufferLog()
|
||||
|
||||
g := prepGenerator(t)
|
||||
g.log = log
|
||||
|
||||
require.NoError(t, g.insertTick(context.Background(), connect(t, g.dsn), _tick))
|
||||
|
||||
seeded := count(t, g.dsn, "deployments")
|
||||
require.Positive(t, seeded)
|
||||
|
||||
g.interval = 10 * time.Millisecond
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(stopCh)
|
||||
|
||||
assert.NoError(t, g.Run(ctx))
|
||||
}()
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
return count(t, g.dsn, "deployments") > seeded
|
||||
}, 60*time.Second, 50*time.Millisecond)
|
||||
|
||||
cancel()
|
||||
|
||||
<-stopCh
|
||||
|
||||
require.NoError(t, wr.Flush())
|
||||
assert.NotContains(t, content(), "backfilling historical data")
|
||||
})
|
||||
|
||||
// a failure inside the tick loop is logged and the loop carries on.
|
||||
t.Run("Tick insert failure is logged", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
lg, wr, content := bufferLog()
|
||||
|
||||
g := prepGenerator(t)
|
||||
g.log = lg
|
||||
|
||||
require.NoError(t, g.insertTick(context.Background(), connect(t, g.dsn), _tick))
|
||||
|
||||
seeded := count(t, g.dsn, "deployments")
|
||||
require.Positive(t, seeded)
|
||||
|
||||
g.interval = 10 * time.Millisecond
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(stopCh)
|
||||
|
||||
assert.NoError(t, g.Run(ctx))
|
||||
}()
|
||||
|
||||
// the table can only be pulled out from under the loop once the
|
||||
// loop is running, which its first successful insert proves.
|
||||
require.Eventually(t, func() bool {
|
||||
return count(t, g.dsn, "deployments") > seeded
|
||||
}, 60*time.Second, 50*time.Millisecond)
|
||||
|
||||
exec(t, g.dsn, "DROP TABLE deployments")
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
require.NoError(t, wr.Flush())
|
||||
|
||||
return strings.Contains(content(), "tick insert failed")
|
||||
}, 60*time.Second, 50*time.Millisecond)
|
||||
|
||||
cancel()
|
||||
|
||||
<-stopCh
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Generator_needsBackfill(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
// Seed indicates whether a row is inserted before the check.
|
||||
Seed bool
|
||||
|
||||
// DropTable removes the table so the query itself fails.
|
||||
DropTable bool
|
||||
|
||||
Result bool
|
||||
Err error
|
||||
}{
|
||||
"Empty database still needs a backfill": {},
|
||||
"Populated database does not": {
|
||||
Seed: true,
|
||||
Result: true,
|
||||
},
|
||||
"Error returned by the query": {
|
||||
DropTable: true,
|
||||
Err: assert.AnError,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := prepGenerator(t)
|
||||
|
||||
if c.Seed {
|
||||
require.NoError(t, g.insertTick(context.Background(), connect(t, g.dsn), _tick))
|
||||
}
|
||||
|
||||
if c.DropTable {
|
||||
exec(t, g.dsn, "DROP TABLE deployments")
|
||||
}
|
||||
|
||||
res, err := g.needsBackfill(context.Background(), connect(t, g.dsn))
|
||||
testutil.AssertEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, c.Result, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Generator_backfill(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
// DropTable removes a table so an insert inside the loop fails.
|
||||
DropTable string
|
||||
|
||||
Err error
|
||||
}{
|
||||
"Successful backfill": {},
|
||||
"Error returned by insertTick": {
|
||||
DropTable: "deployments",
|
||||
Err: assert.AnError,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := prepGenerator(t)
|
||||
|
||||
if c.DropTable != "" {
|
||||
exec(t, g.dsn, "DROP TABLE "+c.DropTable)
|
||||
}
|
||||
|
||||
err := g.backfill(context.Background(), connect(t, g.dsn))
|
||||
testutil.AssertEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// one day of ticks covers a stretch of demo history, so every
|
||||
// table has to have been written to.
|
||||
assert.Positive(t, count(t, g.dsn, "deployments"))
|
||||
assert.Positive(t, count(t, g.dsn, "incidents"))
|
||||
assert.Positive(t, count(t, g.dsn, "build_metrics"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Generator_insertTick(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
// DropTable removes a table so the batch fails on exec.
|
||||
DropTable string
|
||||
|
||||
// EmptyTick indicates the generator should be steered at an instant
|
||||
// that produces nothing at all.
|
||||
EmptyTick bool
|
||||
|
||||
Err error
|
||||
}{
|
||||
"Successful insert": {},
|
||||
"A tick with no rows sends no batch": {
|
||||
EmptyTick: true,
|
||||
},
|
||||
"Error returned by the batch": {
|
||||
DropTable: "deployments",
|
||||
Err: assert.AnError,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := prepGenerator(t)
|
||||
|
||||
if c.DropTable != "" {
|
||||
exec(t, g.dsn, "DROP TABLE "+c.DropTable)
|
||||
}
|
||||
|
||||
at := _tick
|
||||
|
||||
// the early return needs a tick that generates nothing, which
|
||||
// only a specific seed at a quiet instant produces.
|
||||
if c.EmptyTick {
|
||||
seed, quiet := emptyDraw()
|
||||
g.r = mockmetrics.NewRand(seed)
|
||||
at = quiet
|
||||
}
|
||||
|
||||
err := g.insertTick(context.Background(), connect(t, g.dsn), at)
|
||||
testutil.AssertEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
total := count(t, g.dsn, "deployments") +
|
||||
count(t, g.dsn, "incidents") +
|
||||
count(t, g.dsn, "build_metrics")
|
||||
|
||||
if c.EmptyTick {
|
||||
assert.Zero(t, total)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Positive(t, total)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// _tick is a weekday office-hours instant, busy enough that every table gets
|
||||
// rows.
|
||||
var _tick = time.Date(2024, time.January, 9, 11, 0, 0, 0, time.UTC)
|
||||
|
||||
// prepGenerator gives a test its own randomly named database in the shared
|
||||
// container, already carrying the demo schema.
|
||||
func prepGenerator(t *testing.T) *Generator {
|
||||
t.Helper()
|
||||
|
||||
name := "demo_" + strconv.FormatInt(time.Now().UnixNano(), 36) + strconv.Itoa(len(t.Name()))
|
||||
|
||||
admin := connect(t, dsn(_testDB))
|
||||
_, err := admin.Exec(context.Background(), "CREATE DATABASE "+name)
|
||||
require.NoError(t, err)
|
||||
|
||||
conn := connect(t, dsn(name))
|
||||
for _, q := range _schema {
|
||||
_, err = conn.Exec(context.Background(), q)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
g := NewGenerator(dsn(name), slog.New(slog.DiscardHandler))
|
||||
|
||||
// a single day keeps the backfill loop to a few hundred ticks instead
|
||||
// of the month the generator runs with in production.
|
||||
g.backfillDays = 1
|
||||
|
||||
// a pinned seed makes the first tick at _tick fill every table, so no
|
||||
// test depends on a sparse table happening to draw a row.
|
||||
g.r = mockmetrics.NewRand(_fullDrawSeed)
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
// dsn builds a connection string for one database in the container.
|
||||
func dsn(db string) string {
|
||||
return fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", _pgUser, _pgPass, _pgAddr, db)
|
||||
}
|
||||
|
||||
// connect opens a connection that is closed when the test ends.
|
||||
func connect(t *testing.T, dsn string) *pgx.Conn {
|
||||
t.Helper()
|
||||
|
||||
conn, err := pgx.Connect(context.Background(), dsn)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, conn.Close(context.Background()))
|
||||
})
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
// exec runs a single statement against the given database.
|
||||
func exec(t *testing.T, dsn, query string) {
|
||||
t.Helper()
|
||||
|
||||
_, err := connect(t, dsn).Exec(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// count returns the number of rows in a table over a connection of its own,
|
||||
// so the polling callers cannot pile up connections.
|
||||
func count(t *testing.T, dsn, table string) int {
|
||||
t.Helper()
|
||||
|
||||
conn, err := pgx.Connect(context.Background(), dsn)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
assert.NoError(t, conn.Close(context.Background()))
|
||||
}()
|
||||
|
||||
var n int
|
||||
|
||||
require.NoError(t, conn.QueryRow(context.Background(), "SELECT COUNT(*) FROM "+table).Scan(&n))
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// emptyDraw finds a seed and a quiet instant that together generate nothing
|
||||
// at all, so the empty-tick early return can be reached deterministically.
|
||||
func emptyDraw() (int64, time.Time) {
|
||||
at := time.Date(2024, time.January, 7, 3, 0, 0, 0, time.UTC)
|
||||
|
||||
for seed := int64(1); ; seed++ {
|
||||
tick := demodata.GenerateTick(mockmetrics.NewRand(seed), at)
|
||||
if len(tick.Deployments)+len(tick.Incidents)+len(tick.BuildMetrics) == 0 {
|
||||
return seed, at
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bufferLog creates a logger writing into a buffer whose contents the caller
|
||||
// can read back once flushed.
|
||||
func bufferLog() (*slog.Logger, *testutil.Writer, func() string) {
|
||||
wr, buf := testutil.NewBuffer()
|
||||
|
||||
return slog.New(slog.NewJSONHandler(wr, nil)), wr, buf.String
|
||||
}
|
||||
|
||||
// _fullDrawSeed is a seed whose first draw at _tick fills every table, so a
|
||||
// dropped table reliably breaks the batch.
|
||||
var _fullDrawSeed = fullDraw()
|
||||
|
||||
// fullDraw searches for that seed.
|
||||
func fullDraw() int64 {
|
||||
for seed := int64(1); ; seed++ {
|
||||
tick := demodata.GenerateTick(mockmetrics.NewRand(seed), _tick)
|
||||
if len(tick.Deployments) > 0 && len(tick.Incidents) > 0 && len(tick.BuildMetrics) > 0 {
|
||||
return seed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
// Package server exposes the generated demo metrics over HTTP.
|
||||
package server
|
||||
|
||||
import (
|
||||
|
|
@ -15,6 +16,9 @@ import (
|
|||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// _requestTimeout bounds how long a single scrape may take.
|
||||
const _requestTimeout = 30 * time.Second
|
||||
|
||||
// Server exposes metrics over HTTP.
|
||||
type Server struct {
|
||||
log *slog.Logger
|
||||
|
|
@ -71,6 +75,8 @@ func (s *Server) Address() string {
|
|||
// Close performs clean up operations.
|
||||
func (s *Server) Close() error {
|
||||
if err := s.http.Shutdown(context.Background()); err != nil {
|
||||
// NOCOV: shutting down against an unbounded context only fails if
|
||||
// closing a listener does, which cannot be provoked in tests.
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -83,11 +89,11 @@ func (s *Server) Close() error {
|
|||
func (s *Server) router() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(httpserver.Timeout(30 * time.Second))
|
||||
r.Use(httpserver.Timeout(_requestTimeout))
|
||||
r.Use(httpserver.Recoverer(s.log))
|
||||
|
||||
r.Handle("/api/metrics", promhttp.InstrumentMetricHandler(
|
||||
s.fc, promhttp.HandlerFor(s.fc, promhttp.HandlerOpts{}),
|
||||
s.fc, promhttp.HandlerFor(s.fc, promhttp.HandlerOpts{CoalesceGather: true}),
|
||||
))
|
||||
|
||||
r.MethodNotAllowed(httpserver.MethodNotAllowed(s.log))
|
||||
|
|
|
|||
206
datagen/internal/server/server_test.go
Normal file
206
datagen/internal/server/server_test.go
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/oxynote/oxynote/server/core/pkg/metricutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
func Test_New(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
log = slog.New(slog.DiscardHandler)
|
||||
fc = newFactory()
|
||||
)
|
||||
|
||||
s := New(log, fc)
|
||||
require.NotNil(t, s)
|
||||
|
||||
assert.Equal(t, log, s.log)
|
||||
assert.Equal(t, fc, s.fc)
|
||||
|
||||
require.NotNil(t, s.http)
|
||||
assert.Equal(t, ":8080", s.http.Addr)
|
||||
assert.Equal(t, time.Minute, s.http.ReadTimeout)
|
||||
assert.Equal(t, time.Minute, s.http.ReadHeaderTimeout)
|
||||
assert.NotNil(t, s.http.Handler)
|
||||
assert.NotNil(t, s.http.ErrorLog)
|
||||
}
|
||||
|
||||
func Test_Server_Listen(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
log, wr, content := bufferLog()
|
||||
|
||||
s := Server{
|
||||
log: log,
|
||||
http: &http.Server{Addr: ":a", ReadHeaderTimeout: time.Minute},
|
||||
}
|
||||
|
||||
// an invalid port makes ListenAndServe fail immediately.
|
||||
s.Listen()
|
||||
|
||||
require.NoError(t, wr.Flush())
|
||||
assert.Contains(t, content(), `"msg":"listening"`)
|
||||
assert.Contains(t, content(), "cannot terminate gracefully")
|
||||
|
||||
// a graceful shutdown ends Listen without the error log.
|
||||
log2, wr2, content2 := bufferLog()
|
||||
|
||||
s2 := Server{
|
||||
log: log2,
|
||||
http: &http.Server{Addr: "127.0.0.1:0", ReadHeaderTimeout: time.Minute},
|
||||
}
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(stopCh)
|
||||
|
||||
s2.Listen()
|
||||
}()
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
require.NoError(t, wr2.Flush())
|
||||
|
||||
return content2() != ""
|
||||
}, 5*time.Second, 10*time.Millisecond)
|
||||
|
||||
require.NoError(t, s2.http.Shutdown(context.Background()))
|
||||
|
||||
<-stopCh
|
||||
|
||||
require.NoError(t, wr2.Flush())
|
||||
assert.Contains(t, content2(), `"msg":"listening"`)
|
||||
assert.NotContains(t, content2(), "cannot terminate gracefully")
|
||||
}
|
||||
|
||||
func Test_Server_Address(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
Addr string
|
||||
Result string
|
||||
}{
|
||||
"Default HTTP port is omitted": {
|
||||
Addr: ":80",
|
||||
Result: "http://localhost",
|
||||
},
|
||||
"Custom port is included": {
|
||||
Addr: ":1234",
|
||||
Result: "http://localhost:1234",
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := Server{http: &http.Server{Addr: c.Addr, ReadHeaderTimeout: time.Minute}}
|
||||
|
||||
assert.Equal(t, c.Result, s.Address())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Server_Close(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := Server{
|
||||
log: slog.New(slog.DiscardHandler),
|
||||
http: &http.Server{ReadHeaderTimeout: time.Minute},
|
||||
}
|
||||
|
||||
assert.NoError(t, s.Close())
|
||||
}
|
||||
|
||||
func Test_Server_router(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
Method string
|
||||
Path string
|
||||
RespCode int
|
||||
|
||||
// Contains is a fragment the response body must carry, used where
|
||||
// the body is a metrics dump rather than a fixed payload.
|
||||
Contains string
|
||||
}{
|
||||
"Metrics are served": {
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/metrics",
|
||||
RespCode: http.StatusOK,
|
||||
Contains: "engineering_test_gauge",
|
||||
},
|
||||
"Unknown path is not found": {
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/nothing",
|
||||
RespCode: http.StatusNotFound,
|
||||
},
|
||||
// the metrics route is registered for every method, so a scrape
|
||||
// with an odd verb is answered rather than rejected.
|
||||
"Metrics answer any method": {
|
||||
Method: http.MethodDelete,
|
||||
Path: "/api/metrics",
|
||||
RespCode: http.StatusOK,
|
||||
Contains: "engineering_test_gauge",
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fc := newFactory()
|
||||
|
||||
// a registered gauge gives the scrape something to report.
|
||||
fc.NewGauge(metricutil.Options{Name: "test_gauge", Help: "Tracks nothing."}).Set(1)
|
||||
|
||||
s := Server{log: slog.New(slog.DiscardHandler), fc: fc}
|
||||
|
||||
req := httptest.NewRequest(c.Method, "http://test.com"+c.Path, http.NoBody)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
s.router().ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, c.RespCode, rec.Code)
|
||||
|
||||
if c.Contains != "" {
|
||||
assert.Contains(t, rec.Body.String(), c.Contains)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// newFactory creates a metrics factory backed by a registry of its own, so
|
||||
// parallel tests never collide over metric names.
|
||||
func newFactory() metricutil.Factory {
|
||||
return metricutil.NewFactory(
|
||||
"engineering",
|
||||
prometheus.NewRegistry(),
|
||||
metricutil.WithCustomHost("demo"),
|
||||
)
|
||||
}
|
||||
|
||||
// bufferLog creates a logger writing into a buffer whose contents the caller
|
||||
// can read back once flushed.
|
||||
func bufferLog() (*slog.Logger, *testutil.Writer, func() string) {
|
||||
wr, buf := testutil.NewBuffer()
|
||||
|
||||
return slog.New(slog.NewJSONHandler(wr, nil)), wr, buf.String
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
global:
|
||||
scrape_interval: 5s
|
||||
evaluation_interval: 5s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'core'
|
||||
metrics_path: api/x/metrics
|
||||
static_configs:
|
||||
- targets: ['core:8080']
|
||||
- job_name: 'datagen'
|
||||
metrics_path: api/metrics
|
||||
static_configs:
|
||||
- targets: ['datagen:8080']
|
||||
|
|
@ -238,21 +238,6 @@ services:
|
|||
- SCREEN_DEPTH=16
|
||||
- MAX_CONCURRENT_CHROME_PROCESSES=10
|
||||
- LOG_LEVEL=WARNING
|
||||
prometheus:
|
||||
container_name: prometheus
|
||||
image: prom/prometheus:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- prometheus_data:/prometheus
|
||||
- ./demo/prometheus:/etc/prometheus/
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yaml"
|
||||
logging:
|
||||
driver: local
|
||||
options:
|
||||
mode: non-blocking
|
||||
max-size: 10m
|
||||
max-file: "3"
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: grafana
|
||||
|
|
@ -273,5 +258,4 @@ volumes:
|
|||
caddy_data:
|
||||
caddy_config:
|
||||
changedetection_data:
|
||||
prometheus_data:
|
||||
grafana_storage:
|
||||
|
|
|
|||
1
docker/env/core.example.env
vendored
1
docker/env/core.example.env
vendored
|
|
@ -18,7 +18,6 @@ OXYNOTE_CORE_STORAGE_BUCKET=dev
|
|||
OXYNOTE_CORE_SERVER_AUTH_BETTER_AUTH_URL=http://auth-realtime:8081/api/auth/get-session
|
||||
OXYNOTE_CORE_SERVER_MCP_SESSION_URL=http://auth-realtime:8081/api/internal/mcp/session
|
||||
OXYNOTE_CORE_AUTH_REALTIME_URL=http://auth-realtime:8081
|
||||
OXYNOTE_CORE_SERVER_DEMO_PROMETHEUS_URL=http://prometheus:9090
|
||||
# an empty URL disables changedetection.io: url-watcher hooks cannot be
|
||||
# created and existing ones are skipped.
|
||||
OXYNOTE_CORE_CHANGEDETECTION_API_URL=http://changedetection:5000
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
# - no container_name, so this stack coexists with the dev one
|
||||
# - env is inlined and committed; there is nothing secret to protect
|
||||
# - only the services the tests exercise (no changedetection, mariadb,
|
||||
# datagen, prometheus or grafana)
|
||||
# datagen or grafana)
|
||||
#
|
||||
# the front door is the dev Caddyfile verbatim: e2e must exercise the
|
||||
# same routing and the same /api/x and /api/internal blocks that ship.
|
||||
|
|
@ -124,12 +124,6 @@ services:
|
|||
- OXYNOTE_CORE_BASE_APP_URL=http://localhost:18080
|
||||
- OXYNOTE_CORE_ORIGINS=http://localhost:18080,localhost:18080
|
||||
- OXYNOTE_CORE_DB_DATA_SOURCE_CREDENTIALS_SIGNING_SECRET=e2esigningsecret
|
||||
# core only seeds a new workspace's welcome document while a demo
|
||||
# prometheus URL is set — with it empty, initialize returns early and
|
||||
# the onboarding page polls for a first document that never comes.
|
||||
# The URL is stored, not dialed, so there is no prometheus here; a
|
||||
# block that queries it would simply get a connection error.
|
||||
- OXYNOTE_CORE_SERVER_DEMO_PROMETHEUS_URL=http://prometheus.invalid:9090
|
||||
- OXYNOTE_CORE_EMAIL_SMTP_HOST=mailpit
|
||||
- OXYNOTE_CORE_EMAIL_SMTP_PORT=1025
|
||||
- OXYNOTE_CORE_EMAIL_SMTP_TLS=none
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ infrastructure, and the non-Go services.
|
|||
|
||||
- `server/core/` — Go module `github.com/oxynote/oxynote/server/core`. One binary: `cmd/core` (`oxynote-core`) — main API server. Listens on `:8080`. Exposes `/api/...` (auth-required), `/api/apps/...` (public, sessionless third-party callbacks), and `/api/x/...` (internal-only, no auth; the reverse proxy is expected to firewall this). Owns Postgres, Meilisearch, Valkey (Redis), MinIO, the GitHub/Slack apps, the assistant, and the outbound data-source connections (`internal/datasource`).
|
||||
- `server/auth-realtime/` — `@oxynote/auth-realtime` package. TypeScript ES-module service that runs Better Auth (organization plugin) and a Hocuspocus (Yjs CRDT) server in a single Hono process on port `8081`. Forwards non-auth `/api/...` traffic to core (`OXYNOTE_AUTH_REALTIME_BACKEND_URL`).
|
||||
- `datagen/` — separate Go module `github.com/oxynote/oxynote/datagen`. Synthesises demo Postgres/MariaDB content and a fake metrics endpoint scraped by Prometheus for the demo data sources.
|
||||
- `datagen/` — separate Go module `github.com/oxynote/oxynote/datagen`. Synthesises demo Postgres/MariaDB content, and serves a fake metrics endpoint for a Prometheus to scrape. The demo Prometheus data source no longer uses that endpoint: core synthesises those metrics itself (`server/core/internal/datasource/demo`), so nothing in the dev stack scrapes it.
|
||||
|
||||
Go builds go through **goreleaser**: `make build` in `server/core` and `datagen` runs `goreleaser release --snapshot --clean`, producing binaries in `bin/` and the `ghcr.io/oxynote/{core,datagen}:dev` images via each module's `Dockerfile.dev`. The dev compose stack consumes those images; `auth-realtime` (and `web/`) are built from source by compose via their `Dockerfile.dev`. `make run` / `make start` from the repository root orchestrates both.
|
||||
|
||||
|
|
@ -31,8 +31,11 @@ Per-component:
|
|||
# core
|
||||
cd server/core && make build # goreleaser release --snapshot --clean -> bin/
|
||||
|
||||
# datagen
|
||||
# datagen — same targets as core
|
||||
cd datagen && make build
|
||||
cd datagen && make test # go test -race ./... (pgdemo/mariademo need docker)
|
||||
cd datagen && make check-coverage # fails below COVERAGE_MIN
|
||||
cd datagen && make check-lint # golangci-lint; lint = the fixing variant
|
||||
|
||||
# auth-realtime (full details in auth-realtime/CLAUDE.md)
|
||||
cd server/auth-realtime && pnpm run dev # local watch mode (tsx + sentry.ts preloaded)
|
||||
|
|
@ -43,8 +46,12 @@ cd server/auth-realtime && pnpm run test # vitest run --coverage
|
|||
cd server/auth-realtime && pnpm run qa # check-lint + test; qa-fix = lint + test
|
||||
```
|
||||
|
||||
The Go test/lint workflow is documented in [core/CLAUDE.md](core/CLAUDE.md)
|
||||
("Commands & QA gates").
|
||||
Both Go modules share the same test/lint workflow, documented in
|
||||
[core/CLAUDE.md](core/CLAUDE.md) ("Commands & QA gates"); datagen carries its
|
||||
own copy of core's golangci-lint profile. Its `pgdemo` and `mariademo` suites
|
||||
start throwaway Postgres and MariaDB containers, so docker is a unit-test
|
||||
dependency for those two packages the same way it is for core's `internal/db`
|
||||
and `internal/datasource/processor`.
|
||||
|
||||
Go dependencies are fetched into the module cache at build time (`make deps` runs `go mod download`) — there is no vendoring. All dependencies, including the first-party `github.com/oxynote/wetsocks`, are public; no GOPRIVATE or git auth setup is needed.
|
||||
|
||||
|
|
@ -71,6 +78,16 @@ So from a frontend's point of view: auth + realtime is `:8080/auth-realtime/...`
|
|||
|
||||
Most public routes in the README (`/api/documents`, `/api/documents/tree`, etc.) are served by core; the README is the closest thing to a contract spec — when changing handlers, update it.
|
||||
|
||||
## The demo data source
|
||||
|
||||
Every organization is seeded at initialization with a Prometheus data source named "Demo" whose URL is `demo://engineering`, plus the welcome document whose charts query it. This is unconditional — there is no env var and no way to opt out, so a fresh install shows populated charts with nothing else deployed.
|
||||
|
||||
- `internal/datasource/demo` synthesises those metrics in-process. `runner.ensurePrepared` picks it over the real client when the URL is exactly `demo://engineering`; any other `demo://` URL is `demo.ErrUnknownSource` rather than a second demo. The data source's type stays `prometheus`, so the editor, autocomplete and charts cannot tell the difference.
|
||||
- **One `demo.Client` serves the whole process, and it lives on `datasource.Manager`.** The client owns its engine, parser and registry, and the registry's walks cache the history they replay. A runner is built per call, so building the client there instead would throw that cache away on every request and re-walk every tick since the epoch.
|
||||
- **The timeline is a pure function of the tick, not stored data.** A fixed epoch and a one-minute tick, values computed on demand from a per-series seed — so history never rewrites, a query over any window backfills for free, and the same window always answers identically. `walk.checkpoint` caches one value per day-long segment; without it a sample would replay every tick since the epoch.
|
||||
- Queries run through the official `promql` engine over `queryable`, a `storage.Queryable` that synthesises only the requested window. Samples are spaced at the query's own step and the lookback delta is widened to match, which is what lets a year-long range answer with ~100 points per series instead of half a million.
|
||||
- `registry.go` is the single declaration of what exists: ~15 gauge families driving both sample synthesis and the metadata/label/series endpoints. `Test_welcomeDocumentQueries` runs every query in `internal/document/files/available_metrics.json` against it, so a metric renamed in one place and not the other fails the build rather than emptying the welcome document silently.
|
||||
|
||||
## Document storage / Hocuspocus integration
|
||||
|
||||
This is the model that's least obvious from a fresh read:
|
||||
|
|
|
|||
|
|
@ -255,6 +255,21 @@ linters:
|
|||
- linters:
|
||||
- gochecknoglobals
|
||||
text: ^_.+
|
||||
# the demo registry is tuned sampling parameters read as a block: every
|
||||
# bound, target and spike chance is one row of demo tuning, not a
|
||||
# threshold logic branches on. Naming each would bury the table, and a
|
||||
# shared constant would tie together gauges that only coincide today.
|
||||
- linters:
|
||||
- goconst
|
||||
- mnd
|
||||
path: internal/datasource/demo
|
||||
# the demo timeline promises bit-identical replay, so its tests compare
|
||||
# the floats it produces exactly. A tolerance would pass on precisely
|
||||
# the drift the assertions exist to catch.
|
||||
- linters:
|
||||
- testifylint
|
||||
path: internal/datasource/demo
|
||||
text: float-compare
|
||||
- linters:
|
||||
- forcetypeassert
|
||||
- gochecknoinits
|
||||
|
|
|
|||
|
|
@ -289,10 +289,9 @@ func main() { //nolint:maintidx // main performs linear wiring of all components
|
|||
srv, err := server.NewServer(
|
||||
log,
|
||||
server.Options{
|
||||
PublicURL: buildinfo.Getenv("SERVER_PUBLIC_URL"),
|
||||
DemoPrometheusURL: buildinfo.Getenv("SERVER_DEMO_PROMETHEUS_URL"),
|
||||
Port: _serverPort,
|
||||
Origins: origins,
|
||||
PublicURL: buildinfo.Getenv("SERVER_PUBLIC_URL"),
|
||||
Port: _serverPort,
|
||||
Origins: origins,
|
||||
Assistant: server.AssistantCapability{
|
||||
Status: assistantStatus,
|
||||
Model: assistantOpts.Model,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ require (
|
|||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/Masterminds/squirrel v1.5.4
|
||||
github.com/blang/semver/v4 v4.0.0
|
||||
github.com/bradleyfalzon/ghinstallation/v2 v2.18.0
|
||||
github.com/bradleyfalzon/ghinstallation/v2 v2.19.0
|
||||
github.com/cenkalti/backoff/v4 v4.3.0
|
||||
github.com/cloudwego/eino v0.9.15
|
||||
github.com/cloudwego/eino-ext/components/model/claude v0.1.25
|
||||
|
|
@ -17,70 +17,72 @@ require (
|
|||
github.com/coder/websocket v1.8.15
|
||||
github.com/dchest/uniuri v1.2.0
|
||||
github.com/eino-contrib/jsonschema v1.0.3
|
||||
github.com/getsentry/sentry-go v0.46.2
|
||||
github.com/go-chi/chi/v5 v5.3.0
|
||||
github.com/getsentry/sentry-go v0.48.0
|
||||
github.com/go-chi/chi/v5 v5.3.2
|
||||
github.com/go-chi/cors v1.2.2
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/gomodule/redigo v1.9.3
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/go-containerregistry v0.21.6
|
||||
github.com/google/go-github/v72 v72.0.0
|
||||
github.com/google/go-containerregistry v0.22.0
|
||||
github.com/google/go-github/v88 v88.0.0
|
||||
github.com/gorilla/schema v1.4.1
|
||||
github.com/gosimple/slug v1.15.0
|
||||
github.com/guregu/null/v5 v5.0.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/jackc/pgx/v5 v5.9.2
|
||||
github.com/jackc/pgx/v5 v5.10.0
|
||||
github.com/jarcoal/httpmock v1.4.2
|
||||
github.com/jellydator/ttlcache/v3 v3.4.0
|
||||
github.com/jellydator/ttlcache/v3 v3.4.1
|
||||
github.com/jellydator/xync v0.0.0-20240601154136-0f038e166df4
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0
|
||||
github.com/meilisearch/meilisearch-go v0.36.2
|
||||
github.com/minio/minio-go/v7 v7.1.0
|
||||
github.com/meilisearch/meilisearch-go v0.36.3
|
||||
github.com/minio/minio-go/v7 v7.3.0
|
||||
github.com/modelcontextprotocol/go-sdk v1.7.0
|
||||
github.com/orlangure/gnomock v0.32.0
|
||||
github.com/oxynote/wetsocks v0.1.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/prometheus/client_golang v1.24.1
|
||||
github.com/prometheus/client_model v0.6.2
|
||||
github.com/prometheus/common v0.67.5
|
||||
github.com/prometheus/common v0.70.1
|
||||
github.com/prometheus/prometheus v0.314.0
|
||||
github.com/qustavo/sqlhooks/v2 v2.1.0
|
||||
github.com/rafaeljusto/redigomock v2.4.0+incompatible
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rs/xid v1.6.0
|
||||
github.com/rubenv/sql-migrate v1.8.1
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/slack-go/slack v0.23.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/slack-go/slack v0.29.0
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/tidwall/gjson v1.19.0
|
||||
github.com/wneessen/go-mail v0.8.1
|
||||
go.uber.org/goleak v1.3.0
|
||||
google.golang.org/genai v1.36.0
|
||||
google.golang.org/genai v1.69.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.116.0 // indirect
|
||||
cloud.google.com/go/auth v0.9.3 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.7.0 // indirect
|
||||
cloud.google.com/go/auth v0.20.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/anthropics/anthropic-sdk-go v1.56.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.43.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.68 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.20 // indirect
|
||||
github.com/aws/smithy-go v1.22.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 // indirect
|
||||
github.com/aws/smithy-go v1.27.4 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
||||
|
|
@ -94,32 +96,35 @@ require (
|
|||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dennwc/varint v1.0.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/cli v29.5.2+incompatible // indirect
|
||||
github.com/docker/cli v29.7.2+incompatible // indirect
|
||||
github.com/docker/docker v28.5.2+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.7 // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/edsrzf/mmap-go v1.2.1-0.20241212181136-fad1cd13edbd // indirect
|
||||
github.com/eino-contrib/ollama v0.1.0 // indirect
|
||||
github.com/evanphx/json-patch v0.5.2 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect
|
||||
github.com/felixge/httpsnoop v1.1.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/go-github/v84 v84.0.0 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/jsonschema-go v0.4.3 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.18 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.23.0 // indirect
|
||||
github.com/goph/emperror v0.17.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/gosimple/unidecode v1.0.1 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/invopop/jsonschema v0.14.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
|
|
@ -127,8 +132,8 @@ require (
|
|||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jpillora/backoff v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/compress v1.19.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
|
|
@ -139,18 +144,17 @@ require (
|
|||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
||||
github.com/nikolalohinski/gonja v1.5.3 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pb33f/ordered-map/v2 v2.3.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/prometheus/procfs v0.21.1 // indirect
|
||||
github.com/segmentio/asm v1.1.3 // indirect
|
||||
github.com/segmentio/encoding v0.5.4 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
|
|
@ -162,36 +166,46 @@ require (
|
|||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/yargevad/filepathx v1.0.0 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.uber.org/zap v1.28.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect
|
||||
golang.org/x/arch v0.12.0 // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/crypto v0.55.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 // indirect
|
||||
golang.org/x/net v0.56.0 // indirect
|
||||
golang.org/x/net v0.58.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/api v0.197.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
|
||||
google.golang.org/grpc v1.74.2 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
google.golang.org/api v0.290.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260720155508-bb71a54f79dc // indirect
|
||||
google.golang.org/grpc v1.82.1 // indirect
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.3 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apimachinery v0.35.3 // indirect
|
||||
k8s.io/client-go v0.35.3 // indirect
|
||||
k8s.io/klog/v2 v2.140.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.3 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
|
||||
cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
|
||||
cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U=
|
||||
cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
|
||||
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
|
||||
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
|
||||
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
|
||||
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
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/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 h1:CU4+EJeJi3TKYWEcYuSdWsjzw0nVsK/H0MSQOiPcymU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0/go.mod h1:q0+UTSRvShwUCrR/s5HtyInYphN7Wvxb7snFM3u+SLA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
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/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||
|
|
@ -20,40 +26,46 @@ github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA4
|
|||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
|
||||
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=
|
||||
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/anthropics/anthropic-sdk-go v1.56.0 h1:idVU14wOZ06D0GBNEvuhn927xXmBVEquo0469iDwLsc=
|
||||
github.com/anthropics/anthropic-sdk-go v1.56.0/go.mod h1:3EfIfmFqxH6rbiLcIP4tPFyXL/IHakx2wDG4OU+TIEI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
|
||||
github.com/aws/aws-sdk-go-v2 v1.43.0 h1:fharf/WhbRAVZ1du0QL7roNFxZ6T/sWr+4Ni617bwSI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.43.0/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.15 h1:I5XjesVMpDZXZEZonVfjI12VNMrYa38LtLnw4NtY5Ss=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.15/go.mod h1:tNIp4JIPonlsgaO5hxO372a6gjhN63aSWl2GVl5QoBQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.68 h1:cFb9yjI02/sWHBSYXAtkamjzCuRymvmeFmt0TC0MbYY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.68/go.mod h1:H6E+jBzyqUu8u0vGaU6POkK3P0NylYEeRZ6ynBpMqIk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.20 h1:oIaQ1e17CSKaWmUTu62MtraRWVIosn/iONMuZt0gbqc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.20/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
|
||||
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
|
||||
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.31 h1:n4nY9O3QKoHIkL85EX+V8RcMFtOhlpTFhGArg915PXk=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.31/go.mod h1:PN0NYDCCoOpGGsZ2+elDUidmHfQBPyYzN2GCgl8HEBs=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.30 h1:TTCvvzFU6gXa4iJecNG/0F/B0oYTiazoRECr2XyLHrY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.30/go.mod h1:jKxAp2AEncnliinzpgOSZDFv6+VjvWhjw/AtbfsWT9U=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 h1:kfVL5wAunCJycL6MOQ6aNh6PlAYEymflcjuKmrWUA0o=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31/go.mod h1:nWfRNDAppujCQgOUd43lKT4yeLv9z3nJ3bw1G3BgQKo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31 h1:Z8F3hfCY33IGpJjFAnv0wvtv1FIKj1GHmRDEYqy64tw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31/go.mod h1:aVyUoytEyOViR6jhq6jula0xkc5NfBE2hgeF6BvOrao=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31 h1:hyOxUyXdh3AyjE93gBgsfziJag9ACwcs+ZpDBLzi8mw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31/go.mod h1:OERqI9k0draSLB8O8woxY3q25ZWTELRK4RRoLMuMZFo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32 h1:0MrUL35H/Y4kdFfItoR5jCgtDQ4Z/8LudAoIHRfA4hE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32/go.mod h1:2tNZkuWz54arj8mHVf+8Y7cKkcD8Wr/fBpENgEXpjLc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 h1:mbRIur/BiHK6SKPjoBIXSE/hJ6g6JGRLuxQy1jGjlN4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13/go.mod h1:ITg9em2KbJx1s0y4aqRX5OYWG6HBZ5TVR//OdpEZ2CQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31 h1:w2SIhW92DZPFrSL4ksVCr8IYff5OZwIcxg8+95tzvAI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31/go.mod h1:wAhpCQbkov+IcvjozJbd2xRCoZybUEHNkcFunssNACg=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 h1:OHH5iTQvVGmfHjX/5Q+vFuA/Rf2x6/95aJ/75QCQSm4=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.5.0/go.mod h1:mCF3AK9PpL49oOrhniUXWAfhVBVQ/XbytoE5eccZUIs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 h1:CaJyYhxBE0M/HJX/YvSaSmQlsI91VHB0lKU8LtLxL3A=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.33.0/go.mod h1:+e6BMRMPjBQoCw/WovYR9GLy2IU0z4Q77smOB1DraSg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0 h1:tC323YV77QdafeBr6LUhLDTsboyuyHLNRwAyCP44kGU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0/go.mod h1:SfLK1sgviHmbI+MozR9iDwDjL4cdCVZtahsjoR+z7wg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 h1:Pd6PNlp4t8PTXxqzstICl52Wsy78vpjFZ7PRUj44mJc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.45.0/go.mod h1:rmQ0TnHzuLPmabgjPcsywhsSOmaBDgzR4zvDxSPsGdg=
|
||||
github.com/aws/smithy-go v1.27.4 h1:JQcphmBN4f0q/sPqXqROIItRNV/hy10cgu7CsFy616M=
|
||||
github.com/aws/smithy-go v1.27.4/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=
|
||||
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
|
||||
|
|
@ -62,8 +74,8 @@ github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2y
|
|||
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/bradleyfalzon/ghinstallation/v2 v2.18.0 h1:WPqnN6NS9XvYlOgZQAIseN7Z1uAiE+UxgDKlW7FvFuU=
|
||||
github.com/bradleyfalzon/ghinstallation/v2 v2.18.0/go.mod h1:gpoSwwWc4biE49F7n+roCcpkEkZ1Qr9soZ2ESvMiouU=
|
||||
github.com/bradleyfalzon/ghinstallation/v2 v2.19.0 h1:KQfD+43pRw9NUJhGycGrFr9vF1MubZacksKol1gomFI=
|
||||
github.com/bradleyfalzon/ghinstallation/v2 v2.19.0/go.mod h1:fe5ECIhCdEnxwLiBlNTxx9CP455wt42BELnlDVMvaAA=
|
||||
github.com/buger/jsonparser v1.2.0 h1:4EFcvK1kD4jyj6YqNK6skK6w+y7FHHBR+XBCtxwu/6g=
|
||||
github.com/buger/jsonparser v1.2.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
|
||||
|
|
@ -78,11 +90,11 @@ github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiD
|
|||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
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/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
|
||||
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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/cloudwego/eino v0.9.15 h1:F+7uXeZYbJm30a/kaC93Mj6H9K2zx4thQaQP695NggE=
|
||||
|
|
@ -99,7 +111,8 @@ github.com/cloudwego/eino-ext/components/model/openrouter v0.1.10 h1:i5YdCW8omvO
|
|||
github.com/cloudwego/eino-ext/components/model/openrouter v0.1.10/go.mod h1:WRc1OIXVaf2USal59xrIt+05TUCu9P1c8MQh0FcWG8c=
|
||||
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17 h1:EeVcR1TslRA2IdNW1h/2LaGbPlffwGhQm99jM3zWZiI=
|
||||
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17/go.mod h1:Zkcx6DPTR2NfWmtSXbhItswGw6hqUezNPhNcke0pOG8=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
||||
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
|
|
@ -114,12 +127,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
|||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g=
|
||||
github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
|
||||
github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE=
|
||||
github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/docker/cli v29.5.2+incompatible h1:ubykJ1Y8LmNRGJ2BuMQ0kHOt/RO1YzGNswqWMJgivuQ=
|
||||
github.com/docker/cli v29.5.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY=
|
||||
github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.9.7 h1:jaPIxEIDz5bQeghNAdzz0ETwMMnM4vzjZlxz3pWP4JA=
|
||||
|
|
@ -130,39 +145,74 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
|
|||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/edsrzf/mmap-go v1.2.1-0.20241212181136-fad1cd13edbd h1:I4PrRZuNMeDP3VbFrak4QsqwO5tWkQf0tqrrr1L2DsU=
|
||||
github.com/edsrzf/mmap-go v1.2.1-0.20241212181136-fad1cd13edbd/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
|
||||
github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0=
|
||||
github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4=
|
||||
github.com/eino-contrib/ollama v0.1.0 h1:z1NaMdKW6X1ftP8g5xGGR5zDRPUtuTKFq35vBQgxsN4=
|
||||
github.com/eino-contrib/ollama v0.1.0/go.mod h1:mYsQ7b3DeqY8bHPuD3MZJYTqkgyL6LoemxoP/B7ZNhA=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
||||
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM=
|
||||
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc=
|
||||
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
|
||||
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||
github.com/getsentry/sentry-go v0.46.2 h1:1jhYwrKGa3sIpo/y5iDNXS5wDoT7I1KNzMHrnK6ojns=
|
||||
github.com/getsentry/sentry-go v0.46.2/go.mod h1:evVbw2qotNUdYG8KxXbAdjOQWWvWIwKxpjdZZIvcIPw=
|
||||
github.com/getsentry/sentry-go v0.48.0 h1:FRZNr7Uk1C86ev1bSJmYlUkL9oyivQA6YOcdYfaaMmY=
|
||||
github.com/getsentry/sentry-go v0.48.0/go.mod h1:E5UkA5wp1qR2+MDydNYlVeUiNN2xEdjYMidkgf0Qoss=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
|
||||
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||
github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY=
|
||||
github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
|
||||
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
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.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
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-openapi/jsonpointer v0.23.2 h1:DK7R/3zAt4xTytxNkw7jARGPFI7rkaSsii58n8X45x0=
|
||||
github.com/go-openapi/jsonpointer v0.23.2/go.mod h1:noUOckXtq7b4bVkqw0sbHKieq9uEZRN7p6EF/dalc4w=
|
||||
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
|
||||
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
|
||||
github.com/go-openapi/swag v0.26.0 h1:GVDXCmfvhfu1BxiHo8/FA+BbKmhecHnG3varjON5/RI=
|
||||
github.com/go-openapi/swag v0.26.0/go.mod h1:82g3193sZJRbocs7bNCqGfIgq8pkuwVwCfhKIRlEQF0=
|
||||
github.com/go-openapi/swag/cmdutils v0.26.0 h1:iowihOcvq7y4egO8cOq0dmfohz6wfeQ63U1EnuhO2TU=
|
||||
github.com/go-openapi/swag/cmdutils v0.26.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
|
||||
github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I=
|
||||
github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE=
|
||||
github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU=
|
||||
github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc=
|
||||
github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE=
|
||||
github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc=
|
||||
github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA=
|
||||
github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E=
|
||||
github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko=
|
||||
github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg=
|
||||
github.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ=
|
||||
github.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0=
|
||||
github.com/go-openapi/swag/netutils v0.26.0 h1:CmZp+ZT7HrmFwrC3GdGsXBq2+42T1bjKBapcqVpIs3c=
|
||||
github.com/go-openapi/swag/netutils v0.26.0/go.mod h1:5iK+Ok3ZohWWex1C50BFTPexi03UaPwjW4Oj8kgrpwo=
|
||||
github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg=
|
||||
github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE=
|
||||
github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4=
|
||||
github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE=
|
||||
github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ=
|
||||
github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
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=
|
||||
|
|
@ -174,65 +224,51 @@ github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXe
|
|||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
|
||||
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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-containerregistry v0.21.6 h1:T+yqQIlJXKrM98Om4DlW3GoWQAmhZuLMwoDOvVrtiUM=
|
||||
github.com/google/go-containerregistry v0.21.6/go.mod h1:U7MMSBIJynke2MVQrQk19NP9k/uQsGz/h0amIFSHMbo=
|
||||
github.com/google/go-github/v72 v72.0.0 h1:FcIO37BLoVPBO9igQQ6tStsv2asG4IPcYFi655PPvBM=
|
||||
github.com/google/go-github/v72 v72.0.0/go.mod h1:WWtw8GMRiL62mvIquf1kO3onRHeWWKmK01qdCY8c5fg=
|
||||
github.com/google/go-github/v84 v84.0.0 h1:I/0Xn5IuChMe8TdmI2bbim5nyhaRFJ7DEdzmD2w+yVA=
|
||||
github.com/google/go-github/v84 v84.0.0/go.mod h1:WwYL1z1ajRdlaPszjVu/47x1L0PXukJBn73xsiYrRRQ=
|
||||
github.com/google/go-containerregistry v0.22.0 h1:eGbCiPeYxAH/7WLLq6zTBALP0tUIFsoyRauhxXDJ53I=
|
||||
github.com/google/go-containerregistry v0.22.0/go.mod h1:bJR35SK8XgisYmhg/FMQ/5RK0S/XrOAqLBV5/LR2XE0=
|
||||
github.com/google/go-github/v88 v88.0.0 h1:dZA9IKkPK1eXZj4ypngnpRj5FwdpTv4whix2PrQMP7M=
|
||||
github.com/google/go-github/v88 v88.0.0/go.mod h1:rufTDgn2N45wjhukLTyxmvc9nilSp3mr3Rgtt6b1MPw=
|
||||
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
|
||||
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
|
||||
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
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/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.18 h1:hvVi34VucdrV1IIsiWuqYM8kutw/92MxNEFxCJZEh0k=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.18/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k=
|
||||
github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE=
|
||||
github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg=
|
||||
github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18=
|
||||
github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic=
|
||||
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
||||
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
||||
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo=
|
||||
github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
|
||||
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
|
||||
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
|
||||
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM=
|
||||
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||
github.com/guregu/null/v5 v5.0.0 h1:PRxjqyOekS11W+w/7Vfz6jgJE/BCwELWtgvOJzddimw=
|
||||
github.com/guregu/null/v5 v5.0.0/go.mod h1:SjupzNy+sCPtwQTKWhUCqjhVCO69hpsl2QsZrWHjlwU=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
|
|
@ -247,14 +283,14 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
|||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
|
||||
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jarcoal/httpmock v1.4.2 h1:dKwiP/9zITCPfBLsDn3kchbSOu16JrnxtVEmL0fPRcI=
|
||||
github.com/jarcoal/httpmock v1.4.2/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
||||
github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY=
|
||||
github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4=
|
||||
github.com/jellydator/ttlcache/v3 v3.4.1 h1:bOdXmXiycyK6E6Qjyuj5vl+/vU3SCOoDs8a86NbHjAQ=
|
||||
github.com/jellydator/ttlcache/v3 v3.4.1/go.mod h1:j7LO12PNghFg5+0v9budMAT4rDK4JY969jb9vOdOBBk=
|
||||
github.com/jellydator/xync v0.0.0-20240601154136-0f038e166df4 h1:8Pm5dsAGaQ+uuGyB705/7epGUStVLe7DFROYdFTq5aA=
|
||||
github.com/jellydator/xync v0.0.0-20240601154136-0f038e166df4/go.mod h1:cf3z0/QbPKWqY/8YJHkvjiJQx1sjVaJfueetY6YHJJk=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
|
|
@ -268,11 +304,11 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
|
|||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
|
||||
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
|
|
@ -294,10 +330,10 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
|||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M=
|
||||
github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ=
|
||||
github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
|
|
@ -305,16 +341,16 @@ github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX
|
|||
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||
github.com/meguminnnnnnnnn/go-openai v0.1.2 h1:iXombGGjqjBrmE9WaSidUhhi3YQhf42QTHvHLMkgvCA=
|
||||
github.com/meguminnnnnnnnn/go-openai v0.1.2/go.mod h1:qs96ysDmxhE4BZoU45I43zcyfnaYxU3X+aRzLko/htY=
|
||||
github.com/meilisearch/meilisearch-go v0.36.2 h1:MYaMPCpdLh2aYPt+zK+19mLoA4dfBY3S1L7T0FADCjU=
|
||||
github.com/meilisearch/meilisearch-go v0.36.2/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM=
|
||||
github.com/meilisearch/meilisearch-go v0.36.3 h1:Yx1aTY5jDgtbStPVkhJTDoLnZTy5sejQSPyjfNMy6e4=
|
||||
github.com/meilisearch/meilisearch-go v0.36.3/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.1.0 h1:QEt5IStDpxgGjEdtOgpiZ5QhmSl3ax7qy61vi2SwHO8=
|
||||
github.com/minio/minio-go/v7 v7.1.0/go.mod h1:Dm7WS1AgLmBa0NcQD6SeJnJf+K/EUW3GR7Ks6olB3OA=
|
||||
github.com/minio/minio-go/v7 v7.3.0 h1:HM4pFCSQq/TK+j0/zmorSh5ddh81iDgRgU0BG0Vz/YU=
|
||||
github.com/minio/minio-go/v7 v7.3.0/go.mod h1:KUPWdecEO1LWyUz+sTGXAuf2jZHrPh5fCsRH86QbPfk=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
|
|
@ -328,8 +364,9 @@ github.com/modelcontextprotocol/go-sdk v1.7.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K
|
|||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
|
|
@ -338,6 +375,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+
|
|||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c=
|
||||
github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4=
|
||||
github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec=
|
||||
github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
|
|
@ -352,29 +391,40 @@ github.com/oxynote/wetsocks v0.1.0 h1:amd/vv58vNVXgQsR3QFK/HGHHXs6kFu8MBqn1Sp/35
|
|||
github.com/oxynote/wetsocks v0.1.0/go.mod h1:KlgjMvRx1pxooHqEvghbrXPIW1oc7/x03B7uiyMnEAM=
|
||||
github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY=
|
||||
github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
|
||||
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
|
||||
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
|
||||
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
|
||||
github.com/prometheus/client_golang/exp v0.0.0-20260724065723-ecdb8254ba61 h1:SgKx/5u9SwqzZ27E1T4bfuisjTOkI3GagC6WtdEE5lg=
|
||||
github.com/prometheus/client_golang/exp v0.0.0-20260724065723-ecdb8254ba61/go.mod h1:CoLfLGxCH1vzpdmZ+p2uaUGH43j+99HYmnK1Wak6rS4=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY=
|
||||
github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
|
||||
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
|
||||
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
|
||||
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
|
||||
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
|
||||
github.com/prometheus/prometheus v0.314.0 h1:YjsimqsIi6/mOtzZcrPEYUALO6zpfaht9O5sXqDz2vg=
|
||||
github.com/prometheus/prometheus v0.314.0/go.mod h1:zjg3pMTAkY0/JG8jy/h8/YgSQUVB+aCXMhUqN6l64jg=
|
||||
github.com/prometheus/sigv4 v0.4.1 h1:EIc3j+8NBea9u1iV6O5ZAN8uvPq2xOIUPcqCTivHuXs=
|
||||
github.com/prometheus/sigv4 v0.4.1/go.mod h1:eu+ZbRvsc5TPiHwqh77OWuCnWK73IdkETYY46P4dXOU=
|
||||
github.com/qustavo/sqlhooks/v2 v2.1.0 h1:54yBemHnGHp/7xgT+pxwmIlMSDNYKx5JW5dfRAiCZi0=
|
||||
github.com/qustavo/sqlhooks/v2 v2.1.0/go.mod h1:aMREyKo7fOKTwiLuWPsaHRXEmtqG4yREztO0idF83AU=
|
||||
github.com/rafaeljusto/redigomock v2.4.0+incompatible h1:d7uo5MVINMxnRr20MxbgDkmZ8QRfevjOVgEa4n0OZyY=
|
||||
|
|
@ -397,14 +447,16 @@ github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+D
|
|||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/slack-go/slack v0.23.1 h1:ZS5B96wxxYQRwvJ3/vJFtqtUZi3tXhsZCyT44Nv7M80=
|
||||
github.com/slack-go/slack v0.23.1/go.mod h1:H0yR/YBuRJ39RkE+JpV/d/oEsbanzTRowR82bCN0cEs=
|
||||
github.com/slack-go/slack v0.29.0 h1:ohhMNgp9DmPKiLhH/pNZV4NxhOXKgNy0SH8FzVHNerI=
|
||||
github.com/slack-go/slack v0.29.0/go.mod h1:UEe+jmo9WLlwHB04qsOrTDvqM7Aa4rQL3O5wF3n0hx4=
|
||||
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI=
|
||||
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg=
|
||||
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
|
||||
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
|
||||
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
|
||||
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/standard-webhooks/standard-webhooks/libraries v0.0.1 h1:uOfcYT+3QungH6tIGSVCR/Y3KJmgJiHcojJbMTPDZAI=
|
||||
github.com/standard-webhooks/standard-webhooks/libraries v0.0.1/go.mod h1:L1MQhA6x4dn9r007T033lsaZMv9EmBAdXyU/+EF40fo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
|
|
@ -412,18 +464,19 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
|
||||
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
||||
|
|
@ -445,6 +498,8 @@ github.com/wneessen/go-mail v0.8.1 h1:tVcncj02/QySVFw3zr/kXOzZcuFQqBNT6K+Rbgm/pc
|
|||
github.com/wneessen/go-mail v0.8.1/go.mod h1:dWZ61zadzCIyvB4y1/YzC5O7MrbbzBfPkARmbosdf8w=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
|
||||
|
|
@ -455,137 +510,101 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
|||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
||||
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 h1:Mbi5PKN7u322woPa85d7ebZ+SOvEoPvoiBu+ryHWgfA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0/go.mod h1:e7ciERRhZaOZXVjx5MiL8TK5+Xv7G5Gv5PA2ZDEJdL8=
|
||||
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
|
||||
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
||||
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
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/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
|
||||
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
|
||||
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 h1:ex206bKw+v3K0dm3andkrIF+ijyQKJG1pLgwQ2PYdQM=
|
||||
golang.org/x/exp v0.0.0-20260727155853-b88d891fe743/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74=
|
||||
golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.197.0 h1:x6CwqQLsFiA5JKAiGyGBjc2bNtHtLddhJCE2IKuhhcQ=
|
||||
google.golang.org/api v0.197.0/go.mod h1:AuOuo20GoQ331nq7DquGHlU6d+2wN2fZ8O0ta60nRNw=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genai v1.36.0 h1:sJCIjqTAmwrtAIaemtTiKkg2TO1RxnYEusTmEQ3nGxM=
|
||||
google.golang.org/genai v1.36.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/api v0.290.0 h1:eMw0Xo+IfbbMlKmW7aHvpyQRv9RCXuWx/vs8AD+0x9A=
|
||||
google.golang.org/api v0.290.0/go.mod h1:weJZ3lldHFYI0DBFNKpJelUDNnusTt5YaOEgxvt8ci8=
|
||||
google.golang.org/genai v1.69.0 h1:quP3Rbiz0Mn+zPfXsWHQQwOx8IfO2MnQehZUbrJ/jPo=
|
||||
google.golang.org/genai v1.69.0/go.mod h1:mDdPDFXo1Ats7f1WXVyZgWb/CkMzFWTWJruIMy7hGIU=
|
||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
|
||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260724162435-b2f20204f0df h1:NsJx+hCSwIBI6+C4BuJIkb8xOG1M+nfQDsqIrQHT92k=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260724162435-b2f20204f0df/go.mod h1:1brfde68Npq6+WA75c1EHWPijZEG1kMus61ygPZfn4A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260720155508-bb71a54f79dc h1:3TtNq/QbJNrSY1nVdjcikfBw6ujnaNbdrd88wNr1OW4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260720155508-bb71a54f79dc/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw=
|
||||
gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
@ -596,5 +615,23 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
|
||||
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ=
|
||||
k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4=
|
||||
k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8=
|
||||
k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||
k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg=
|
||||
k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c=
|
||||
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.3 h1:u08YRbVUi59ri4YD6cg0UqNM4Dimn0sIl+wldcx5PYw=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.3/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
|
|
|||
|
|
@ -275,10 +275,10 @@ var _ meilisearch.IndexManager = &MeiliIndexManager{}
|
|||
// GetSortableAttributesWithContextFunc: func(ctx context.Context) (*[]string, error) {
|
||||
// panic("mock out the GetSortableAttributesWithContext method")
|
||||
// },
|
||||
// GetStatsFunc: func() (*meilisearch.StatsIndex, error) {
|
||||
// GetStatsFunc: func(param *meilisearch.StatsParams) (*meilisearch.StatsIndex, error) {
|
||||
// panic("mock out the GetStats method")
|
||||
// },
|
||||
// GetStatsWithContextFunc: func(ctx context.Context) (*meilisearch.StatsIndex, error) {
|
||||
// GetStatsWithContextFunc: func(ctx context.Context, param *meilisearch.StatsParams) (*meilisearch.StatsIndex, error) {
|
||||
// panic("mock out the GetStatsWithContext method")
|
||||
// },
|
||||
// GetStopWordsFunc: func() (*[]string, error) {
|
||||
|
|
@ -898,10 +898,10 @@ type MeiliIndexManager struct {
|
|||
GetSortableAttributesWithContextFunc func(ctx context.Context) (*[]string, error)
|
||||
|
||||
// GetStatsFunc mocks the GetStats method.
|
||||
GetStatsFunc func() (*meilisearch.StatsIndex, error)
|
||||
GetStatsFunc func(param *meilisearch.StatsParams) (*meilisearch.StatsIndex, error)
|
||||
|
||||
// GetStatsWithContextFunc mocks the GetStatsWithContext method.
|
||||
GetStatsWithContextFunc func(ctx context.Context) (*meilisearch.StatsIndex, error)
|
||||
GetStatsWithContextFunc func(ctx context.Context, param *meilisearch.StatsParams) (*meilisearch.StatsIndex, error)
|
||||
|
||||
// GetStopWordsFunc mocks the GetStopWords method.
|
||||
GetStopWordsFunc func() (*[]string, error)
|
||||
|
|
@ -1750,11 +1750,15 @@ type MeiliIndexManager struct {
|
|||
}
|
||||
// GetStats holds details about calls to the GetStats method.
|
||||
GetStats []struct {
|
||||
// Param is the param argument value.
|
||||
Param *meilisearch.StatsParams
|
||||
}
|
||||
// GetStatsWithContext holds details about calls to the GetStatsWithContext method.
|
||||
GetStatsWithContext []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// Param is the param argument value.
|
||||
Param *meilisearch.StatsParams
|
||||
}
|
||||
// GetStopWords holds details about calls to the GetStopWords method.
|
||||
GetStopWords []struct {
|
||||
|
|
@ -5751,9 +5755,12 @@ func (mock *MeiliIndexManager) GetSortableAttributesWithContextCalls() []struct
|
|||
}
|
||||
|
||||
// GetStats calls GetStatsFunc.
|
||||
func (mock *MeiliIndexManager) GetStats() (*meilisearch.StatsIndex, error) {
|
||||
func (mock *MeiliIndexManager) GetStats(param *meilisearch.StatsParams) (*meilisearch.StatsIndex, error) {
|
||||
callInfo := struct {
|
||||
}{}
|
||||
Param *meilisearch.StatsParams
|
||||
}{
|
||||
Param: param,
|
||||
}
|
||||
mock.lockGetStats.Lock()
|
||||
mock.calls.GetStats = append(mock.calls.GetStats, callInfo)
|
||||
mock.lockGetStats.Unlock()
|
||||
|
|
@ -5764,7 +5771,7 @@ func (mock *MeiliIndexManager) GetStats() (*meilisearch.StatsIndex, error) {
|
|||
)
|
||||
return statsIndexOut, errOut
|
||||
}
|
||||
return mock.GetStatsFunc()
|
||||
return mock.GetStatsFunc(param)
|
||||
}
|
||||
|
||||
// GetStatsCalls gets all the calls that were made to GetStats.
|
||||
|
|
@ -5772,8 +5779,10 @@ func (mock *MeiliIndexManager) GetStats() (*meilisearch.StatsIndex, error) {
|
|||
//
|
||||
// len(mockedIndexManager.GetStatsCalls())
|
||||
func (mock *MeiliIndexManager) GetStatsCalls() []struct {
|
||||
Param *meilisearch.StatsParams
|
||||
} {
|
||||
var calls []struct {
|
||||
Param *meilisearch.StatsParams
|
||||
}
|
||||
mock.lockGetStats.RLock()
|
||||
calls = mock.calls.GetStats
|
||||
|
|
@ -5782,11 +5791,13 @@ func (mock *MeiliIndexManager) GetStatsCalls() []struct {
|
|||
}
|
||||
|
||||
// GetStatsWithContext calls GetStatsWithContextFunc.
|
||||
func (mock *MeiliIndexManager) GetStatsWithContext(ctx context.Context) (*meilisearch.StatsIndex, error) {
|
||||
func (mock *MeiliIndexManager) GetStatsWithContext(ctx context.Context, param *meilisearch.StatsParams) (*meilisearch.StatsIndex, error) {
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
Ctx context.Context
|
||||
Param *meilisearch.StatsParams
|
||||
}{
|
||||
Ctx: ctx,
|
||||
Ctx: ctx,
|
||||
Param: param,
|
||||
}
|
||||
mock.lockGetStatsWithContext.Lock()
|
||||
mock.calls.GetStatsWithContext = append(mock.calls.GetStatsWithContext, callInfo)
|
||||
|
|
@ -5798,7 +5809,7 @@ func (mock *MeiliIndexManager) GetStatsWithContext(ctx context.Context) (*meilis
|
|||
)
|
||||
return statsIndexOut, errOut
|
||||
}
|
||||
return mock.GetStatsWithContextFunc(ctx)
|
||||
return mock.GetStatsWithContextFunc(ctx, param)
|
||||
}
|
||||
|
||||
// GetStatsWithContextCalls gets all the calls that were made to GetStatsWithContext.
|
||||
|
|
@ -5806,10 +5817,12 @@ func (mock *MeiliIndexManager) GetStatsWithContext(ctx context.Context) (*meilis
|
|||
//
|
||||
// len(mockedIndexManager.GetStatsWithContextCalls())
|
||||
func (mock *MeiliIndexManager) GetStatsWithContextCalls() []struct {
|
||||
Ctx context.Context
|
||||
Ctx context.Context
|
||||
Param *meilisearch.StatsParams
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
Ctx context.Context
|
||||
Param *meilisearch.StatsParams
|
||||
}
|
||||
mock.lockGetStatsWithContext.RLock()
|
||||
calls = mock.calls.GetStatsWithContext
|
||||
|
|
|
|||
|
|
@ -84,6 +84,12 @@ var _ meilisearch.ServiceManager = &MeiliServiceManager{}
|
|||
// DeleteKeyWithContextFunc: func(ctx context.Context, keyOrUID string) (bool, error) {
|
||||
// panic("mock out the DeleteKeyWithContext method")
|
||||
// },
|
||||
// DeleteSearchRuleFunc: func(uid string) error {
|
||||
// panic("mock out the DeleteSearchRule method")
|
||||
// },
|
||||
// DeleteSearchRuleWithContextFunc: func(ctx context.Context, uid string) error {
|
||||
// panic("mock out the DeleteSearchRuleWithContext method")
|
||||
// },
|
||||
// DeleteTasksFunc: func(param *meilisearch.DeleteTasksQuery) (*meilisearch.TaskInfo, error) {
|
||||
// panic("mock out the DeleteTasks method")
|
||||
// },
|
||||
|
|
@ -168,15 +174,27 @@ var _ meilisearch.ServiceManager = &MeiliServiceManager{}
|
|||
// GetRawIndexesWithContextFunc: func(ctx context.Context, param *meilisearch.IndexesQuery) (map[string]interface{}, error) {
|
||||
// panic("mock out the GetRawIndexesWithContext method")
|
||||
// },
|
||||
// GetStatsFunc: func() (*meilisearch.Stats, error) {
|
||||
// GetSearchRuleFunc: func(uid string) (*meilisearch.SearchRule, error) {
|
||||
// panic("mock out the GetSearchRule method")
|
||||
// },
|
||||
// GetSearchRuleWithContextFunc: func(ctx context.Context, uid string) (*meilisearch.SearchRule, error) {
|
||||
// panic("mock out the GetSearchRuleWithContext method")
|
||||
// },
|
||||
// GetStatsFunc: func(param *meilisearch.StatsParams) (*meilisearch.Stats, error) {
|
||||
// panic("mock out the GetStats method")
|
||||
// },
|
||||
// GetStatsWithContextFunc: func(ctx context.Context) (*meilisearch.Stats, error) {
|
||||
// GetStatsWithContextFunc: func(ctx context.Context, param *meilisearch.StatsParams) (*meilisearch.Stats, error) {
|
||||
// panic("mock out the GetStatsWithContext method")
|
||||
// },
|
||||
// GetTaskFunc: func(taskUID int64) (*meilisearch.Task, error) {
|
||||
// panic("mock out the GetTask method")
|
||||
// },
|
||||
// GetTaskDocumentsFunc: func(taskUID int64, dst interface{}) error {
|
||||
// panic("mock out the GetTaskDocuments method")
|
||||
// },
|
||||
// GetTaskDocumentsWithContextFunc: func(ctx context.Context, taskUID int64, dst interface{}) error {
|
||||
// panic("mock out the GetTaskDocumentsWithContext method")
|
||||
// },
|
||||
// GetTaskWithContextFunc: func(ctx context.Context, taskUID int64) (*meilisearch.Task, error) {
|
||||
// panic("mock out the GetTaskWithContext method")
|
||||
// },
|
||||
|
|
@ -222,6 +240,12 @@ var _ meilisearch.ServiceManager = &MeiliServiceManager{}
|
|||
// ListIndexesWithContextFunc: func(ctx context.Context, param *meilisearch.IndexesQuery) (*meilisearch.IndexesResults, error) {
|
||||
// panic("mock out the ListIndexesWithContext method")
|
||||
// },
|
||||
// ListSearchRulesFunc: func(params *meilisearch.SearchRulesParams) (*meilisearch.SearchRulesResults, error) {
|
||||
// panic("mock out the ListSearchRules method")
|
||||
// },
|
||||
// ListSearchRulesWithContextFunc: func(ctx context.Context, params *meilisearch.SearchRulesParams) (*meilisearch.SearchRulesResults, error) {
|
||||
// panic("mock out the ListSearchRulesWithContext method")
|
||||
// },
|
||||
// ListWebhooksFunc: func() (*meilisearch.WebhookResults, error) {
|
||||
// panic("mock out the ListWebhooks method")
|
||||
// },
|
||||
|
|
@ -240,6 +264,12 @@ var _ meilisearch.ServiceManager = &MeiliServiceManager{}
|
|||
// ResetChatWorkspaceWithContextFunc: func(ctx context.Context, uid string) (*meilisearch.ChatWorkspaceSettings, error) {
|
||||
// panic("mock out the ResetChatWorkspaceWithContext method")
|
||||
// },
|
||||
// SearchRulesManagerFunc: func() meilisearch.SearchRulesManager {
|
||||
// panic("mock out the SearchRulesManager method")
|
||||
// },
|
||||
// SearchRulesReaderFunc: func() meilisearch.SearchRulesReader {
|
||||
// panic("mock out the SearchRulesReader method")
|
||||
// },
|
||||
// ServiceReaderFunc: func() meilisearch.ServiceReader {
|
||||
// panic("mock out the ServiceReader method")
|
||||
// },
|
||||
|
|
@ -273,6 +303,12 @@ var _ meilisearch.ServiceManager = &MeiliServiceManager{}
|
|||
// UpdateNetworkWithContextFunc: func(ctx context.Context, params *meilisearch.UpdateNetworkRequest) (any, error) {
|
||||
// panic("mock out the UpdateNetworkWithContext method")
|
||||
// },
|
||||
// UpdateSearchRuleFunc: func(uid string, params *meilisearch.SearchRulesRequest) (*meilisearch.SearchRule, error) {
|
||||
// panic("mock out the UpdateSearchRule method")
|
||||
// },
|
||||
// UpdateSearchRuleWithContextFunc: func(ctx context.Context, uid string, params *meilisearch.SearchRulesRequest) (*meilisearch.SearchRule, error) {
|
||||
// panic("mock out the UpdateSearchRuleWithContext method")
|
||||
// },
|
||||
// UpdateWebhookFunc: func(uuid string, params *meilisearch.UpdateWebhookRequest) (*meilisearch.Webhook, error) {
|
||||
// panic("mock out the UpdateWebhook method")
|
||||
// },
|
||||
|
|
@ -367,6 +403,12 @@ type MeiliServiceManager struct {
|
|||
// DeleteKeyWithContextFunc mocks the DeleteKeyWithContext method.
|
||||
DeleteKeyWithContextFunc func(ctx context.Context, keyOrUID string) (bool, error)
|
||||
|
||||
// DeleteSearchRuleFunc mocks the DeleteSearchRule method.
|
||||
DeleteSearchRuleFunc func(uid string) error
|
||||
|
||||
// DeleteSearchRuleWithContextFunc mocks the DeleteSearchRuleWithContext method.
|
||||
DeleteSearchRuleWithContextFunc func(ctx context.Context, uid string) error
|
||||
|
||||
// DeleteTasksFunc mocks the DeleteTasks method.
|
||||
DeleteTasksFunc func(param *meilisearch.DeleteTasksQuery) (*meilisearch.TaskInfo, error)
|
||||
|
||||
|
|
@ -451,15 +493,27 @@ type MeiliServiceManager struct {
|
|||
// GetRawIndexesWithContextFunc mocks the GetRawIndexesWithContext method.
|
||||
GetRawIndexesWithContextFunc func(ctx context.Context, param *meilisearch.IndexesQuery) (map[string]interface{}, error)
|
||||
|
||||
// GetSearchRuleFunc mocks the GetSearchRule method.
|
||||
GetSearchRuleFunc func(uid string) (*meilisearch.SearchRule, error)
|
||||
|
||||
// GetSearchRuleWithContextFunc mocks the GetSearchRuleWithContext method.
|
||||
GetSearchRuleWithContextFunc func(ctx context.Context, uid string) (*meilisearch.SearchRule, error)
|
||||
|
||||
// GetStatsFunc mocks the GetStats method.
|
||||
GetStatsFunc func() (*meilisearch.Stats, error)
|
||||
GetStatsFunc func(param *meilisearch.StatsParams) (*meilisearch.Stats, error)
|
||||
|
||||
// GetStatsWithContextFunc mocks the GetStatsWithContext method.
|
||||
GetStatsWithContextFunc func(ctx context.Context) (*meilisearch.Stats, error)
|
||||
GetStatsWithContextFunc func(ctx context.Context, param *meilisearch.StatsParams) (*meilisearch.Stats, error)
|
||||
|
||||
// GetTaskFunc mocks the GetTask method.
|
||||
GetTaskFunc func(taskUID int64) (*meilisearch.Task, error)
|
||||
|
||||
// GetTaskDocumentsFunc mocks the GetTaskDocuments method.
|
||||
GetTaskDocumentsFunc func(taskUID int64, dst interface{}) error
|
||||
|
||||
// GetTaskDocumentsWithContextFunc mocks the GetTaskDocumentsWithContext method.
|
||||
GetTaskDocumentsWithContextFunc func(ctx context.Context, taskUID int64, dst interface{}) error
|
||||
|
||||
// GetTaskWithContextFunc mocks the GetTaskWithContext method.
|
||||
GetTaskWithContextFunc func(ctx context.Context, taskUID int64) (*meilisearch.Task, error)
|
||||
|
||||
|
|
@ -505,6 +559,12 @@ type MeiliServiceManager struct {
|
|||
// ListIndexesWithContextFunc mocks the ListIndexesWithContext method.
|
||||
ListIndexesWithContextFunc func(ctx context.Context, param *meilisearch.IndexesQuery) (*meilisearch.IndexesResults, error)
|
||||
|
||||
// ListSearchRulesFunc mocks the ListSearchRules method.
|
||||
ListSearchRulesFunc func(params *meilisearch.SearchRulesParams) (*meilisearch.SearchRulesResults, error)
|
||||
|
||||
// ListSearchRulesWithContextFunc mocks the ListSearchRulesWithContext method.
|
||||
ListSearchRulesWithContextFunc func(ctx context.Context, params *meilisearch.SearchRulesParams) (*meilisearch.SearchRulesResults, error)
|
||||
|
||||
// ListWebhooksFunc mocks the ListWebhooks method.
|
||||
ListWebhooksFunc func() (*meilisearch.WebhookResults, error)
|
||||
|
||||
|
|
@ -523,6 +583,12 @@ type MeiliServiceManager struct {
|
|||
// ResetChatWorkspaceWithContextFunc mocks the ResetChatWorkspaceWithContext method.
|
||||
ResetChatWorkspaceWithContextFunc func(ctx context.Context, uid string) (*meilisearch.ChatWorkspaceSettings, error)
|
||||
|
||||
// SearchRulesManagerFunc mocks the SearchRulesManager method.
|
||||
SearchRulesManagerFunc func() meilisearch.SearchRulesManager
|
||||
|
||||
// SearchRulesReaderFunc mocks the SearchRulesReader method.
|
||||
SearchRulesReaderFunc func() meilisearch.SearchRulesReader
|
||||
|
||||
// ServiceReaderFunc mocks the ServiceReader method.
|
||||
ServiceReaderFunc func() meilisearch.ServiceReader
|
||||
|
||||
|
|
@ -556,6 +622,12 @@ type MeiliServiceManager struct {
|
|||
// UpdateNetworkWithContextFunc mocks the UpdateNetworkWithContext method.
|
||||
UpdateNetworkWithContextFunc func(ctx context.Context, params *meilisearch.UpdateNetworkRequest) (any, error)
|
||||
|
||||
// UpdateSearchRuleFunc mocks the UpdateSearchRule method.
|
||||
UpdateSearchRuleFunc func(uid string, params *meilisearch.SearchRulesRequest) (*meilisearch.SearchRule, error)
|
||||
|
||||
// UpdateSearchRuleWithContextFunc mocks the UpdateSearchRuleWithContext method.
|
||||
UpdateSearchRuleWithContextFunc func(ctx context.Context, uid string, params *meilisearch.SearchRulesRequest) (*meilisearch.SearchRule, error)
|
||||
|
||||
// UpdateWebhookFunc mocks the UpdateWebhook method.
|
||||
UpdateWebhookFunc func(uuid string, params *meilisearch.UpdateWebhookRequest) (*meilisearch.Webhook, error)
|
||||
|
||||
|
|
@ -695,6 +767,18 @@ type MeiliServiceManager struct {
|
|||
// KeyOrUID is the keyOrUID argument value.
|
||||
KeyOrUID string
|
||||
}
|
||||
// DeleteSearchRule holds details about calls to the DeleteSearchRule method.
|
||||
DeleteSearchRule []struct {
|
||||
// UID is the uid argument value.
|
||||
UID string
|
||||
}
|
||||
// DeleteSearchRuleWithContext holds details about calls to the DeleteSearchRuleWithContext method.
|
||||
DeleteSearchRuleWithContext []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// UID is the uid argument value.
|
||||
UID string
|
||||
}
|
||||
// DeleteTasks holds details about calls to the DeleteTasks method.
|
||||
DeleteTasks []struct {
|
||||
// Param is the param argument value.
|
||||
|
|
@ -859,19 +943,51 @@ type MeiliServiceManager struct {
|
|||
// Param is the param argument value.
|
||||
Param *meilisearch.IndexesQuery
|
||||
}
|
||||
// GetSearchRule holds details about calls to the GetSearchRule method.
|
||||
GetSearchRule []struct {
|
||||
// UID is the uid argument value.
|
||||
UID string
|
||||
}
|
||||
// GetSearchRuleWithContext holds details about calls to the GetSearchRuleWithContext method.
|
||||
GetSearchRuleWithContext []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// UID is the uid argument value.
|
||||
UID string
|
||||
}
|
||||
// GetStats holds details about calls to the GetStats method.
|
||||
GetStats []struct {
|
||||
// Param is the param argument value.
|
||||
Param *meilisearch.StatsParams
|
||||
}
|
||||
// GetStatsWithContext holds details about calls to the GetStatsWithContext method.
|
||||
GetStatsWithContext []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// Param is the param argument value.
|
||||
Param *meilisearch.StatsParams
|
||||
}
|
||||
// GetTask holds details about calls to the GetTask method.
|
||||
GetTask []struct {
|
||||
// TaskUID is the taskUID argument value.
|
||||
TaskUID int64
|
||||
}
|
||||
// GetTaskDocuments holds details about calls to the GetTaskDocuments method.
|
||||
GetTaskDocuments []struct {
|
||||
// TaskUID is the taskUID argument value.
|
||||
TaskUID int64
|
||||
// Dst is the dst argument value.
|
||||
Dst interface{}
|
||||
}
|
||||
// GetTaskDocumentsWithContext holds details about calls to the GetTaskDocumentsWithContext method.
|
||||
GetTaskDocumentsWithContext []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// TaskUID is the taskUID argument value.
|
||||
TaskUID int64
|
||||
// Dst is the dst argument value.
|
||||
Dst interface{}
|
||||
}
|
||||
// GetTaskWithContext holds details about calls to the GetTaskWithContext method.
|
||||
GetTaskWithContext []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
|
|
@ -949,6 +1065,18 @@ type MeiliServiceManager struct {
|
|||
// Param is the param argument value.
|
||||
Param *meilisearch.IndexesQuery
|
||||
}
|
||||
// ListSearchRules holds details about calls to the ListSearchRules method.
|
||||
ListSearchRules []struct {
|
||||
// Params is the params argument value.
|
||||
Params *meilisearch.SearchRulesParams
|
||||
}
|
||||
// ListSearchRulesWithContext holds details about calls to the ListSearchRulesWithContext method.
|
||||
ListSearchRulesWithContext []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// Params is the params argument value.
|
||||
Params *meilisearch.SearchRulesParams
|
||||
}
|
||||
// ListWebhooks holds details about calls to the ListWebhooks method.
|
||||
ListWebhooks []struct {
|
||||
}
|
||||
|
|
@ -981,6 +1109,12 @@ type MeiliServiceManager struct {
|
|||
// UID is the uid argument value.
|
||||
UID string
|
||||
}
|
||||
// SearchRulesManager holds details about calls to the SearchRulesManager method.
|
||||
SearchRulesManager []struct {
|
||||
}
|
||||
// SearchRulesReader holds details about calls to the SearchRulesReader method.
|
||||
SearchRulesReader []struct {
|
||||
}
|
||||
// ServiceReader holds details about calls to the ServiceReader method.
|
||||
ServiceReader []struct {
|
||||
}
|
||||
|
|
@ -1046,6 +1180,22 @@ type MeiliServiceManager struct {
|
|||
// Params is the params argument value.
|
||||
Params *meilisearch.UpdateNetworkRequest
|
||||
}
|
||||
// UpdateSearchRule holds details about calls to the UpdateSearchRule method.
|
||||
UpdateSearchRule []struct {
|
||||
// UID is the uid argument value.
|
||||
UID string
|
||||
// Params is the params argument value.
|
||||
Params *meilisearch.SearchRulesRequest
|
||||
}
|
||||
// UpdateSearchRuleWithContext holds details about calls to the UpdateSearchRuleWithContext method.
|
||||
UpdateSearchRuleWithContext []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// UID is the uid argument value.
|
||||
UID string
|
||||
// Params is the params argument value.
|
||||
Params *meilisearch.SearchRulesRequest
|
||||
}
|
||||
// UpdateWebhook holds details about calls to the UpdateWebhook method.
|
||||
UpdateWebhook []struct {
|
||||
// UUID is the uuid argument value.
|
||||
|
|
@ -1114,6 +1264,8 @@ type MeiliServiceManager struct {
|
|||
lockDeleteIndexWithContext sync.RWMutex
|
||||
lockDeleteKey sync.RWMutex
|
||||
lockDeleteKeyWithContext sync.RWMutex
|
||||
lockDeleteSearchRule sync.RWMutex
|
||||
lockDeleteSearchRuleWithContext sync.RWMutex
|
||||
lockDeleteTasks sync.RWMutex
|
||||
lockDeleteTasksWithContext sync.RWMutex
|
||||
lockDeleteWebhook sync.RWMutex
|
||||
|
|
@ -1142,9 +1294,13 @@ type MeiliServiceManager struct {
|
|||
lockGetRawIndexWithContext sync.RWMutex
|
||||
lockGetRawIndexes sync.RWMutex
|
||||
lockGetRawIndexesWithContext sync.RWMutex
|
||||
lockGetSearchRule sync.RWMutex
|
||||
lockGetSearchRuleWithContext sync.RWMutex
|
||||
lockGetStats sync.RWMutex
|
||||
lockGetStatsWithContext sync.RWMutex
|
||||
lockGetTask sync.RWMutex
|
||||
lockGetTaskDocuments sync.RWMutex
|
||||
lockGetTaskDocumentsWithContext sync.RWMutex
|
||||
lockGetTaskWithContext sync.RWMutex
|
||||
lockGetTasks sync.RWMutex
|
||||
lockGetTasksWithContext sync.RWMutex
|
||||
|
|
@ -1160,12 +1316,16 @@ type MeiliServiceManager struct {
|
|||
lockListChatWorkspacesWithContext sync.RWMutex
|
||||
lockListIndexes sync.RWMutex
|
||||
lockListIndexesWithContext sync.RWMutex
|
||||
lockListSearchRules sync.RWMutex
|
||||
lockListSearchRulesWithContext sync.RWMutex
|
||||
lockListWebhooks sync.RWMutex
|
||||
lockListWebhooksWithContext sync.RWMutex
|
||||
lockMultiSearch sync.RWMutex
|
||||
lockMultiSearchWithContext sync.RWMutex
|
||||
lockResetChatWorkspace sync.RWMutex
|
||||
lockResetChatWorkspaceWithContext sync.RWMutex
|
||||
lockSearchRulesManager sync.RWMutex
|
||||
lockSearchRulesReader sync.RWMutex
|
||||
lockServiceReader sync.RWMutex
|
||||
lockSwapIndexes sync.RWMutex
|
||||
lockSwapIndexesWithContext sync.RWMutex
|
||||
|
|
@ -1177,6 +1337,8 @@ type MeiliServiceManager struct {
|
|||
lockUpdateKeyWithContext sync.RWMutex
|
||||
lockUpdateNetwork sync.RWMutex
|
||||
lockUpdateNetworkWithContext sync.RWMutex
|
||||
lockUpdateSearchRule sync.RWMutex
|
||||
lockUpdateSearchRuleWithContext sync.RWMutex
|
||||
lockUpdateWebhook sync.RWMutex
|
||||
lockUpdateWebhookWithContext sync.RWMutex
|
||||
lockVersion sync.RWMutex
|
||||
|
|
@ -1948,6 +2110,80 @@ func (mock *MeiliServiceManager) DeleteKeyWithContextCalls() []struct {
|
|||
return calls
|
||||
}
|
||||
|
||||
// DeleteSearchRule calls DeleteSearchRuleFunc.
|
||||
func (mock *MeiliServiceManager) DeleteSearchRule(uid string) error {
|
||||
callInfo := struct {
|
||||
UID string
|
||||
}{
|
||||
UID: uid,
|
||||
}
|
||||
mock.lockDeleteSearchRule.Lock()
|
||||
mock.calls.DeleteSearchRule = append(mock.calls.DeleteSearchRule, callInfo)
|
||||
mock.lockDeleteSearchRule.Unlock()
|
||||
if mock.DeleteSearchRuleFunc == nil {
|
||||
var (
|
||||
errOut error
|
||||
)
|
||||
return errOut
|
||||
}
|
||||
return mock.DeleteSearchRuleFunc(uid)
|
||||
}
|
||||
|
||||
// DeleteSearchRuleCalls gets all the calls that were made to DeleteSearchRule.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedServiceManager.DeleteSearchRuleCalls())
|
||||
func (mock *MeiliServiceManager) DeleteSearchRuleCalls() []struct {
|
||||
UID string
|
||||
} {
|
||||
var calls []struct {
|
||||
UID string
|
||||
}
|
||||
mock.lockDeleteSearchRule.RLock()
|
||||
calls = mock.calls.DeleteSearchRule
|
||||
mock.lockDeleteSearchRule.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// DeleteSearchRuleWithContext calls DeleteSearchRuleWithContextFunc.
|
||||
func (mock *MeiliServiceManager) DeleteSearchRuleWithContext(ctx context.Context, uid string) error {
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
UID string
|
||||
}{
|
||||
Ctx: ctx,
|
||||
UID: uid,
|
||||
}
|
||||
mock.lockDeleteSearchRuleWithContext.Lock()
|
||||
mock.calls.DeleteSearchRuleWithContext = append(mock.calls.DeleteSearchRuleWithContext, callInfo)
|
||||
mock.lockDeleteSearchRuleWithContext.Unlock()
|
||||
if mock.DeleteSearchRuleWithContextFunc == nil {
|
||||
var (
|
||||
errOut error
|
||||
)
|
||||
return errOut
|
||||
}
|
||||
return mock.DeleteSearchRuleWithContextFunc(ctx, uid)
|
||||
}
|
||||
|
||||
// DeleteSearchRuleWithContextCalls gets all the calls that were made to DeleteSearchRuleWithContext.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedServiceManager.DeleteSearchRuleWithContextCalls())
|
||||
func (mock *MeiliServiceManager) DeleteSearchRuleWithContextCalls() []struct {
|
||||
Ctx context.Context
|
||||
UID string
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
UID string
|
||||
}
|
||||
mock.lockDeleteSearchRuleWithContext.RLock()
|
||||
calls = mock.calls.DeleteSearchRuleWithContext
|
||||
mock.lockDeleteSearchRuleWithContext.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// DeleteTasks calls DeleteTasksFunc.
|
||||
func (mock *MeiliServiceManager) DeleteTasks(param *meilisearch.DeleteTasksQuery) (*meilisearch.TaskInfo, error) {
|
||||
callInfo := struct {
|
||||
|
|
@ -2999,10 +3235,89 @@ func (mock *MeiliServiceManager) GetRawIndexesWithContextCalls() []struct {
|
|||
return calls
|
||||
}
|
||||
|
||||
// GetStats calls GetStatsFunc.
|
||||
func (mock *MeiliServiceManager) GetStats() (*meilisearch.Stats, error) {
|
||||
// GetSearchRule calls GetSearchRuleFunc.
|
||||
func (mock *MeiliServiceManager) GetSearchRule(uid string) (*meilisearch.SearchRule, error) {
|
||||
callInfo := struct {
|
||||
}{}
|
||||
UID string
|
||||
}{
|
||||
UID: uid,
|
||||
}
|
||||
mock.lockGetSearchRule.Lock()
|
||||
mock.calls.GetSearchRule = append(mock.calls.GetSearchRule, callInfo)
|
||||
mock.lockGetSearchRule.Unlock()
|
||||
if mock.GetSearchRuleFunc == nil {
|
||||
var (
|
||||
searchRuleOut *meilisearch.SearchRule
|
||||
errOut error
|
||||
)
|
||||
return searchRuleOut, errOut
|
||||
}
|
||||
return mock.GetSearchRuleFunc(uid)
|
||||
}
|
||||
|
||||
// GetSearchRuleCalls gets all the calls that were made to GetSearchRule.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedServiceManager.GetSearchRuleCalls())
|
||||
func (mock *MeiliServiceManager) GetSearchRuleCalls() []struct {
|
||||
UID string
|
||||
} {
|
||||
var calls []struct {
|
||||
UID string
|
||||
}
|
||||
mock.lockGetSearchRule.RLock()
|
||||
calls = mock.calls.GetSearchRule
|
||||
mock.lockGetSearchRule.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// GetSearchRuleWithContext calls GetSearchRuleWithContextFunc.
|
||||
func (mock *MeiliServiceManager) GetSearchRuleWithContext(ctx context.Context, uid string) (*meilisearch.SearchRule, error) {
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
UID string
|
||||
}{
|
||||
Ctx: ctx,
|
||||
UID: uid,
|
||||
}
|
||||
mock.lockGetSearchRuleWithContext.Lock()
|
||||
mock.calls.GetSearchRuleWithContext = append(mock.calls.GetSearchRuleWithContext, callInfo)
|
||||
mock.lockGetSearchRuleWithContext.Unlock()
|
||||
if mock.GetSearchRuleWithContextFunc == nil {
|
||||
var (
|
||||
searchRuleOut *meilisearch.SearchRule
|
||||
errOut error
|
||||
)
|
||||
return searchRuleOut, errOut
|
||||
}
|
||||
return mock.GetSearchRuleWithContextFunc(ctx, uid)
|
||||
}
|
||||
|
||||
// GetSearchRuleWithContextCalls gets all the calls that were made to GetSearchRuleWithContext.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedServiceManager.GetSearchRuleWithContextCalls())
|
||||
func (mock *MeiliServiceManager) GetSearchRuleWithContextCalls() []struct {
|
||||
Ctx context.Context
|
||||
UID string
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
UID string
|
||||
}
|
||||
mock.lockGetSearchRuleWithContext.RLock()
|
||||
calls = mock.calls.GetSearchRuleWithContext
|
||||
mock.lockGetSearchRuleWithContext.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// GetStats calls GetStatsFunc.
|
||||
func (mock *MeiliServiceManager) GetStats(param *meilisearch.StatsParams) (*meilisearch.Stats, error) {
|
||||
callInfo := struct {
|
||||
Param *meilisearch.StatsParams
|
||||
}{
|
||||
Param: param,
|
||||
}
|
||||
mock.lockGetStats.Lock()
|
||||
mock.calls.GetStats = append(mock.calls.GetStats, callInfo)
|
||||
mock.lockGetStats.Unlock()
|
||||
|
|
@ -3013,7 +3328,7 @@ func (mock *MeiliServiceManager) GetStats() (*meilisearch.Stats, error) {
|
|||
)
|
||||
return statsOut, errOut
|
||||
}
|
||||
return mock.GetStatsFunc()
|
||||
return mock.GetStatsFunc(param)
|
||||
}
|
||||
|
||||
// GetStatsCalls gets all the calls that were made to GetStats.
|
||||
|
|
@ -3021,8 +3336,10 @@ func (mock *MeiliServiceManager) GetStats() (*meilisearch.Stats, error) {
|
|||
//
|
||||
// len(mockedServiceManager.GetStatsCalls())
|
||||
func (mock *MeiliServiceManager) GetStatsCalls() []struct {
|
||||
Param *meilisearch.StatsParams
|
||||
} {
|
||||
var calls []struct {
|
||||
Param *meilisearch.StatsParams
|
||||
}
|
||||
mock.lockGetStats.RLock()
|
||||
calls = mock.calls.GetStats
|
||||
|
|
@ -3031,11 +3348,13 @@ func (mock *MeiliServiceManager) GetStatsCalls() []struct {
|
|||
}
|
||||
|
||||
// GetStatsWithContext calls GetStatsWithContextFunc.
|
||||
func (mock *MeiliServiceManager) GetStatsWithContext(ctx context.Context) (*meilisearch.Stats, error) {
|
||||
func (mock *MeiliServiceManager) GetStatsWithContext(ctx context.Context, param *meilisearch.StatsParams) (*meilisearch.Stats, error) {
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
Ctx context.Context
|
||||
Param *meilisearch.StatsParams
|
||||
}{
|
||||
Ctx: ctx,
|
||||
Ctx: ctx,
|
||||
Param: param,
|
||||
}
|
||||
mock.lockGetStatsWithContext.Lock()
|
||||
mock.calls.GetStatsWithContext = append(mock.calls.GetStatsWithContext, callInfo)
|
||||
|
|
@ -3047,7 +3366,7 @@ func (mock *MeiliServiceManager) GetStatsWithContext(ctx context.Context) (*meil
|
|||
)
|
||||
return statsOut, errOut
|
||||
}
|
||||
return mock.GetStatsWithContextFunc(ctx)
|
||||
return mock.GetStatsWithContextFunc(ctx, param)
|
||||
}
|
||||
|
||||
// GetStatsWithContextCalls gets all the calls that were made to GetStatsWithContext.
|
||||
|
|
@ -3055,10 +3374,12 @@ func (mock *MeiliServiceManager) GetStatsWithContext(ctx context.Context) (*meil
|
|||
//
|
||||
// len(mockedServiceManager.GetStatsWithContextCalls())
|
||||
func (mock *MeiliServiceManager) GetStatsWithContextCalls() []struct {
|
||||
Ctx context.Context
|
||||
Ctx context.Context
|
||||
Param *meilisearch.StatsParams
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
Ctx context.Context
|
||||
Param *meilisearch.StatsParams
|
||||
}
|
||||
mock.lockGetStatsWithContext.RLock()
|
||||
calls = mock.calls.GetStatsWithContext
|
||||
|
|
@ -3102,6 +3423,88 @@ func (mock *MeiliServiceManager) GetTaskCalls() []struct {
|
|||
return calls
|
||||
}
|
||||
|
||||
// GetTaskDocuments calls GetTaskDocumentsFunc.
|
||||
func (mock *MeiliServiceManager) GetTaskDocuments(taskUID int64, dst interface{}) error {
|
||||
callInfo := struct {
|
||||
TaskUID int64
|
||||
Dst interface{}
|
||||
}{
|
||||
TaskUID: taskUID,
|
||||
Dst: dst,
|
||||
}
|
||||
mock.lockGetTaskDocuments.Lock()
|
||||
mock.calls.GetTaskDocuments = append(mock.calls.GetTaskDocuments, callInfo)
|
||||
mock.lockGetTaskDocuments.Unlock()
|
||||
if mock.GetTaskDocumentsFunc == nil {
|
||||
var (
|
||||
errOut error
|
||||
)
|
||||
return errOut
|
||||
}
|
||||
return mock.GetTaskDocumentsFunc(taskUID, dst)
|
||||
}
|
||||
|
||||
// GetTaskDocumentsCalls gets all the calls that were made to GetTaskDocuments.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedServiceManager.GetTaskDocumentsCalls())
|
||||
func (mock *MeiliServiceManager) GetTaskDocumentsCalls() []struct {
|
||||
TaskUID int64
|
||||
Dst interface{}
|
||||
} {
|
||||
var calls []struct {
|
||||
TaskUID int64
|
||||
Dst interface{}
|
||||
}
|
||||
mock.lockGetTaskDocuments.RLock()
|
||||
calls = mock.calls.GetTaskDocuments
|
||||
mock.lockGetTaskDocuments.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// GetTaskDocumentsWithContext calls GetTaskDocumentsWithContextFunc.
|
||||
func (mock *MeiliServiceManager) GetTaskDocumentsWithContext(ctx context.Context, taskUID int64, dst interface{}) error {
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
TaskUID int64
|
||||
Dst interface{}
|
||||
}{
|
||||
Ctx: ctx,
|
||||
TaskUID: taskUID,
|
||||
Dst: dst,
|
||||
}
|
||||
mock.lockGetTaskDocumentsWithContext.Lock()
|
||||
mock.calls.GetTaskDocumentsWithContext = append(mock.calls.GetTaskDocumentsWithContext, callInfo)
|
||||
mock.lockGetTaskDocumentsWithContext.Unlock()
|
||||
if mock.GetTaskDocumentsWithContextFunc == nil {
|
||||
var (
|
||||
errOut error
|
||||
)
|
||||
return errOut
|
||||
}
|
||||
return mock.GetTaskDocumentsWithContextFunc(ctx, taskUID, dst)
|
||||
}
|
||||
|
||||
// GetTaskDocumentsWithContextCalls gets all the calls that were made to GetTaskDocumentsWithContext.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedServiceManager.GetTaskDocumentsWithContextCalls())
|
||||
func (mock *MeiliServiceManager) GetTaskDocumentsWithContextCalls() []struct {
|
||||
Ctx context.Context
|
||||
TaskUID int64
|
||||
Dst interface{}
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
TaskUID int64
|
||||
Dst interface{}
|
||||
}
|
||||
mock.lockGetTaskDocumentsWithContext.RLock()
|
||||
calls = mock.calls.GetTaskDocumentsWithContext
|
||||
mock.lockGetTaskDocumentsWithContext.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// GetTaskWithContext calls GetTaskWithContextFunc.
|
||||
func (mock *MeiliServiceManager) GetTaskWithContext(ctx context.Context, taskUID int64) (*meilisearch.Task, error) {
|
||||
callInfo := struct {
|
||||
|
|
@ -3638,6 +4041,82 @@ func (mock *MeiliServiceManager) ListIndexesWithContextCalls() []struct {
|
|||
return calls
|
||||
}
|
||||
|
||||
// ListSearchRules calls ListSearchRulesFunc.
|
||||
func (mock *MeiliServiceManager) ListSearchRules(params *meilisearch.SearchRulesParams) (*meilisearch.SearchRulesResults, error) {
|
||||
callInfo := struct {
|
||||
Params *meilisearch.SearchRulesParams
|
||||
}{
|
||||
Params: params,
|
||||
}
|
||||
mock.lockListSearchRules.Lock()
|
||||
mock.calls.ListSearchRules = append(mock.calls.ListSearchRules, callInfo)
|
||||
mock.lockListSearchRules.Unlock()
|
||||
if mock.ListSearchRulesFunc == nil {
|
||||
var (
|
||||
searchRulesResultsOut *meilisearch.SearchRulesResults
|
||||
errOut error
|
||||
)
|
||||
return searchRulesResultsOut, errOut
|
||||
}
|
||||
return mock.ListSearchRulesFunc(params)
|
||||
}
|
||||
|
||||
// ListSearchRulesCalls gets all the calls that were made to ListSearchRules.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedServiceManager.ListSearchRulesCalls())
|
||||
func (mock *MeiliServiceManager) ListSearchRulesCalls() []struct {
|
||||
Params *meilisearch.SearchRulesParams
|
||||
} {
|
||||
var calls []struct {
|
||||
Params *meilisearch.SearchRulesParams
|
||||
}
|
||||
mock.lockListSearchRules.RLock()
|
||||
calls = mock.calls.ListSearchRules
|
||||
mock.lockListSearchRules.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// ListSearchRulesWithContext calls ListSearchRulesWithContextFunc.
|
||||
func (mock *MeiliServiceManager) ListSearchRulesWithContext(ctx context.Context, params *meilisearch.SearchRulesParams) (*meilisearch.SearchRulesResults, error) {
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
Params *meilisearch.SearchRulesParams
|
||||
}{
|
||||
Ctx: ctx,
|
||||
Params: params,
|
||||
}
|
||||
mock.lockListSearchRulesWithContext.Lock()
|
||||
mock.calls.ListSearchRulesWithContext = append(mock.calls.ListSearchRulesWithContext, callInfo)
|
||||
mock.lockListSearchRulesWithContext.Unlock()
|
||||
if mock.ListSearchRulesWithContextFunc == nil {
|
||||
var (
|
||||
searchRulesResultsOut *meilisearch.SearchRulesResults
|
||||
errOut error
|
||||
)
|
||||
return searchRulesResultsOut, errOut
|
||||
}
|
||||
return mock.ListSearchRulesWithContextFunc(ctx, params)
|
||||
}
|
||||
|
||||
// ListSearchRulesWithContextCalls gets all the calls that were made to ListSearchRulesWithContext.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedServiceManager.ListSearchRulesWithContextCalls())
|
||||
func (mock *MeiliServiceManager) ListSearchRulesWithContextCalls() []struct {
|
||||
Ctx context.Context
|
||||
Params *meilisearch.SearchRulesParams
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
Params *meilisearch.SearchRulesParams
|
||||
}
|
||||
mock.lockListSearchRulesWithContext.RLock()
|
||||
calls = mock.calls.ListSearchRulesWithContext
|
||||
mock.lockListSearchRulesWithContext.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// ListWebhooks calls ListWebhooksFunc.
|
||||
func (mock *MeiliServiceManager) ListWebhooks() (*meilisearch.WebhookResults, error) {
|
||||
callInfo := struct {
|
||||
|
|
@ -3857,6 +4336,66 @@ func (mock *MeiliServiceManager) ResetChatWorkspaceWithContextCalls() []struct {
|
|||
return calls
|
||||
}
|
||||
|
||||
// SearchRulesManager calls SearchRulesManagerFunc.
|
||||
func (mock *MeiliServiceManager) SearchRulesManager() meilisearch.SearchRulesManager {
|
||||
callInfo := struct {
|
||||
}{}
|
||||
mock.lockSearchRulesManager.Lock()
|
||||
mock.calls.SearchRulesManager = append(mock.calls.SearchRulesManager, callInfo)
|
||||
mock.lockSearchRulesManager.Unlock()
|
||||
if mock.SearchRulesManagerFunc == nil {
|
||||
var (
|
||||
searchRulesManagerOut meilisearch.SearchRulesManager
|
||||
)
|
||||
return searchRulesManagerOut
|
||||
}
|
||||
return mock.SearchRulesManagerFunc()
|
||||
}
|
||||
|
||||
// SearchRulesManagerCalls gets all the calls that were made to SearchRulesManager.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedServiceManager.SearchRulesManagerCalls())
|
||||
func (mock *MeiliServiceManager) SearchRulesManagerCalls() []struct {
|
||||
} {
|
||||
var calls []struct {
|
||||
}
|
||||
mock.lockSearchRulesManager.RLock()
|
||||
calls = mock.calls.SearchRulesManager
|
||||
mock.lockSearchRulesManager.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// SearchRulesReader calls SearchRulesReaderFunc.
|
||||
func (mock *MeiliServiceManager) SearchRulesReader() meilisearch.SearchRulesReader {
|
||||
callInfo := struct {
|
||||
}{}
|
||||
mock.lockSearchRulesReader.Lock()
|
||||
mock.calls.SearchRulesReader = append(mock.calls.SearchRulesReader, callInfo)
|
||||
mock.lockSearchRulesReader.Unlock()
|
||||
if mock.SearchRulesReaderFunc == nil {
|
||||
var (
|
||||
searchRulesReaderOut meilisearch.SearchRulesReader
|
||||
)
|
||||
return searchRulesReaderOut
|
||||
}
|
||||
return mock.SearchRulesReaderFunc()
|
||||
}
|
||||
|
||||
// SearchRulesReaderCalls gets all the calls that were made to SearchRulesReader.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedServiceManager.SearchRulesReaderCalls())
|
||||
func (mock *MeiliServiceManager) SearchRulesReaderCalls() []struct {
|
||||
} {
|
||||
var calls []struct {
|
||||
}
|
||||
mock.lockSearchRulesReader.RLock()
|
||||
calls = mock.calls.SearchRulesReader
|
||||
mock.lockSearchRulesReader.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// ServiceReader calls ServiceReaderFunc.
|
||||
func (mock *MeiliServiceManager) ServiceReader() meilisearch.ServiceReader {
|
||||
callInfo := struct {
|
||||
|
|
@ -4267,6 +4806,90 @@ func (mock *MeiliServiceManager) UpdateNetworkWithContextCalls() []struct {
|
|||
return calls
|
||||
}
|
||||
|
||||
// UpdateSearchRule calls UpdateSearchRuleFunc.
|
||||
func (mock *MeiliServiceManager) UpdateSearchRule(uid string, params *meilisearch.SearchRulesRequest) (*meilisearch.SearchRule, error) {
|
||||
callInfo := struct {
|
||||
UID string
|
||||
Params *meilisearch.SearchRulesRequest
|
||||
}{
|
||||
UID: uid,
|
||||
Params: params,
|
||||
}
|
||||
mock.lockUpdateSearchRule.Lock()
|
||||
mock.calls.UpdateSearchRule = append(mock.calls.UpdateSearchRule, callInfo)
|
||||
mock.lockUpdateSearchRule.Unlock()
|
||||
if mock.UpdateSearchRuleFunc == nil {
|
||||
var (
|
||||
searchRuleOut *meilisearch.SearchRule
|
||||
errOut error
|
||||
)
|
||||
return searchRuleOut, errOut
|
||||
}
|
||||
return mock.UpdateSearchRuleFunc(uid, params)
|
||||
}
|
||||
|
||||
// UpdateSearchRuleCalls gets all the calls that were made to UpdateSearchRule.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedServiceManager.UpdateSearchRuleCalls())
|
||||
func (mock *MeiliServiceManager) UpdateSearchRuleCalls() []struct {
|
||||
UID string
|
||||
Params *meilisearch.SearchRulesRequest
|
||||
} {
|
||||
var calls []struct {
|
||||
UID string
|
||||
Params *meilisearch.SearchRulesRequest
|
||||
}
|
||||
mock.lockUpdateSearchRule.RLock()
|
||||
calls = mock.calls.UpdateSearchRule
|
||||
mock.lockUpdateSearchRule.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// UpdateSearchRuleWithContext calls UpdateSearchRuleWithContextFunc.
|
||||
func (mock *MeiliServiceManager) UpdateSearchRuleWithContext(ctx context.Context, uid string, params *meilisearch.SearchRulesRequest) (*meilisearch.SearchRule, error) {
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
UID string
|
||||
Params *meilisearch.SearchRulesRequest
|
||||
}{
|
||||
Ctx: ctx,
|
||||
UID: uid,
|
||||
Params: params,
|
||||
}
|
||||
mock.lockUpdateSearchRuleWithContext.Lock()
|
||||
mock.calls.UpdateSearchRuleWithContext = append(mock.calls.UpdateSearchRuleWithContext, callInfo)
|
||||
mock.lockUpdateSearchRuleWithContext.Unlock()
|
||||
if mock.UpdateSearchRuleWithContextFunc == nil {
|
||||
var (
|
||||
searchRuleOut *meilisearch.SearchRule
|
||||
errOut error
|
||||
)
|
||||
return searchRuleOut, errOut
|
||||
}
|
||||
return mock.UpdateSearchRuleWithContextFunc(ctx, uid, params)
|
||||
}
|
||||
|
||||
// UpdateSearchRuleWithContextCalls gets all the calls that were made to UpdateSearchRuleWithContext.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedServiceManager.UpdateSearchRuleWithContextCalls())
|
||||
func (mock *MeiliServiceManager) UpdateSearchRuleWithContextCalls() []struct {
|
||||
Ctx context.Context
|
||||
UID string
|
||||
Params *meilisearch.SearchRulesRequest
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
UID string
|
||||
Params *meilisearch.SearchRulesRequest
|
||||
}
|
||||
mock.lockUpdateSearchRuleWithContext.RLock()
|
||||
calls = mock.calls.UpdateSearchRuleWithContext
|
||||
mock.lockUpdateSearchRuleWithContext.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// UpdateWebhook calls UpdateWebhookFunc.
|
||||
func (mock *MeiliServiceManager) UpdateWebhook(uuid string, params *meilisearch.UpdateWebhookRequest) (*meilisearch.Webhook, error) {
|
||||
callInfo := struct {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import (
|
|||
"slices"
|
||||
"strings"
|
||||
|
||||
gogithub "github.com/google/go-github/v72/github"
|
||||
gogithub "github.com/google/go-github/v88/github"
|
||||
)
|
||||
|
||||
// TreeItemType represents the type of a tree item, either a file or a folder.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package github
|
|||
import (
|
||||
"testing"
|
||||
|
||||
gogithub "github.com/google/go-github/v72/github"
|
||||
gogithub "github.com/google/go-github/v88/github"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/bradleyfalzon/ghinstallation/v2"
|
||||
gogithub "github.com/google/go-github/v72/github"
|
||||
gogithub "github.com/google/go-github/v88/github"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/cryptoutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/errutil"
|
||||
)
|
||||
|
|
@ -193,11 +193,15 @@ func (m *Manager) createInstallationClient(installationID int64) (*gogithub.Clie
|
|||
return nil, fmt.Errorf("failed to create installation transport: %w", err)
|
||||
}
|
||||
|
||||
return gogithub.NewClient(
|
||||
&http.Client{
|
||||
Transport: rt,
|
||||
},
|
||||
), nil
|
||||
client, err := gogithub.NewClient(gogithub.WithHTTPClient(&http.Client{
|
||||
Transport: rt,
|
||||
}))
|
||||
if err != nil {
|
||||
// NOCOV: WithHTTPClient only rejects a nil client.
|
||||
return nil, fmt.Errorf("failed to create github client: %w", err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// InstallationClient represents a GitHub App installation client.
|
||||
|
|
@ -476,11 +480,15 @@ func createAppClient(appID int64, privateKeyPath string) (*gogithub.Client, erro
|
|||
return nil, fmt.Errorf("failed to create installation transport: %w", err)
|
||||
}
|
||||
|
||||
return gogithub.NewClient(
|
||||
&http.Client{
|
||||
Transport: rt,
|
||||
},
|
||||
), nil
|
||||
client, err := gogithub.NewClient(gogithub.WithHTTPClient(&http.Client{
|
||||
Transport: rt,
|
||||
}))
|
||||
if err != nil {
|
||||
// NOCOV: WithHTTPClient only rejects a nil client.
|
||||
return nil, fmt.Errorf("failed to create github client: %w", err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// parseGithubError parses errors returned by the Github API and maps them to appropriate HTTP status codes.
|
||||
|
|
|
|||
|
|
@ -7,11 +7,10 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
gogithub "github.com/google/go-github/v72/github"
|
||||
gogithub "github.com/google/go-github/v88/github"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/cryptoutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/errutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
|
|
@ -37,10 +36,17 @@ func pointManagerAt(t *testing.T, man *Manager, handler http.Handler) {
|
|||
srv := httptest.NewServer(handler)
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
base, err := url.Parse(srv.URL + "/")
|
||||
base := srv.URL + "/"
|
||||
|
||||
// the base URL is fixed at construction, so the client is rebuilt
|
||||
// around the original one's transport instead of being re-pointed.
|
||||
client, err := gogithub.NewClient(
|
||||
gogithub.WithHTTPClient(man.appClient.Client()),
|
||||
gogithub.WithURLs(&base, &base),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
man.appClient.BaseURL = base
|
||||
man.appClient = client
|
||||
}
|
||||
|
||||
// newTestInstallationClient creates an InstallationClient talking to a test
|
||||
|
|
@ -51,13 +57,14 @@ func newTestInstallationClient(t *testing.T, user bool, handler http.Handler) *I
|
|||
srv := httptest.NewServer(handler)
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
client := gogithub.NewClient(srv.Client())
|
||||
base := srv.URL + "/"
|
||||
|
||||
base, err := url.Parse(srv.URL + "/")
|
||||
client, err := gogithub.NewClient(
|
||||
gogithub.WithHTTPClient(srv.Client()),
|
||||
gogithub.WithURLs(&base, &base),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
client.BaseURL = base
|
||||
|
||||
return &InstallationClient{
|
||||
user: user,
|
||||
owner: "own",
|
||||
|
|
|
|||
|
|
@ -218,9 +218,6 @@ func (m *Manager) ProcessNotification(ctx context.Context, n notification.Notifi
|
|||
|
||||
// VerifyMiddleware verifies the Slack request signature.
|
||||
func (m *Manager) VerifyMiddleware(r *http.Request) error {
|
||||
// an empty SignatureSecret would make the verifier accept signatures
|
||||
// computed over an empty key, so refuse outright rather than trusting
|
||||
// the route middleware to be the only gate.
|
||||
if !m.Configured() {
|
||||
return ErrNotConfigured
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ func newGemini(ctx context.Context, opts Options) (model.ToolCallingChatModel, e
|
|||
Backend: genai.BackendGeminiAPI,
|
||||
HTTPOptions: genai.HTTPOptions{
|
||||
Timeout: new(opts.timeout()),
|
||||
// the SDK does not retry at all when this is nil; the empty
|
||||
// struct enables its defaults (5 attempts, exponential
|
||||
// backoff on 408/429/5xx), matching the retry behavior the
|
||||
// other providers' SDKs ship out of the box.
|
||||
RetryOptions: &genai.HTTPRetryOptions{},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -36,6 +41,13 @@ func newGemini(ctx context.Context, opts Options) (model.ToolCallingChatModel, e
|
|||
Model: opts.Model,
|
||||
MaxTokens: opts.maxTokens(),
|
||||
Temperature: opts.Temperature,
|
||||
// eino maps thought parts to ReasoningContent, which the chat
|
||||
// protocol renders as the transient thinking stream; gemini-3
|
||||
// models additionally require thought parts to be restored on
|
||||
// the way back to the API.
|
||||
ThinkingConfig: &genai.ThinkingConfig{
|
||||
IncludeThoughts: true,
|
||||
},
|
||||
}
|
||||
|
||||
cm, err := gemini.NewChatModel(ctx, cfg)
|
||||
|
|
|
|||
|
|
@ -32,10 +32,14 @@ func Test_newGemini(t *testing.T) {
|
|||
APIKey: "k",
|
||||
Backend: genai.BackendGeminiAPI,
|
||||
HTTPOptions: genai.HTTPOptions{
|
||||
Timeout: new(_defaultRequestTimeout),
|
||||
Timeout: new(_defaultRequestTimeout),
|
||||
RetryOptions: &genai.HTTPRetryOptions{},
|
||||
},
|
||||
},
|
||||
Config: &gemini.Config{Model: "gemini-2.5-pro"},
|
||||
Config: &gemini.Config{
|
||||
Model: "gemini-2.5-pro",
|
||||
ThinkingConfig: &genai.ThinkingConfig{IncludeThoughts: true},
|
||||
},
|
||||
},
|
||||
"Optional tuning": {
|
||||
Opts: tunedOptions(ProviderGoogle, "gemini-2.5-pro"),
|
||||
|
|
@ -43,14 +47,16 @@ func Test_newGemini(t *testing.T) {
|
|||
APIKey: "k",
|
||||
Backend: genai.BackendGeminiAPI,
|
||||
HTTPOptions: genai.HTTPOptions{
|
||||
BaseURL: "https://example.invalid/v1",
|
||||
Timeout: new(time.Minute),
|
||||
BaseURL: "https://example.invalid/v1",
|
||||
Timeout: new(time.Minute),
|
||||
RetryOptions: &genai.HTTPRetryOptions{},
|
||||
},
|
||||
},
|
||||
Config: &gemini.Config{
|
||||
Model: "gemini-2.5-pro",
|
||||
MaxTokens: new(1024),
|
||||
Temperature: new(float32(0.3)),
|
||||
Model: "gemini-2.5-pro",
|
||||
MaxTokens: new(1024),
|
||||
Temperature: new(float32(0.3)),
|
||||
ThinkingConfig: &genai.ThinkingConfig{IncludeThoughts: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
261
server/core/internal/datasource/demo/client.go
Normal file
261
server/core/internal/datasource/demo/client.go
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
package demo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/oxynote/oxynote/server/core/internal/datasource/processor"
|
||||
v1 "github.com/prometheus/client_golang/api/prometheus/v1"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
"github.com/prometheus/prometheus/promql/parser"
|
||||
)
|
||||
|
||||
// _queryTimeout bounds one evaluation, matching what the data source
|
||||
// processors give a real server.
|
||||
var _queryTimeout = 10 * time.Second
|
||||
|
||||
// _maxSamples caps how many samples one evaluation may load. The demo's
|
||||
// own series are far below it, so it only ever stops a query whose
|
||||
// expansion — a subquery over a long window — would otherwise grow
|
||||
// without bound.
|
||||
const _maxSamples = 5_000_000
|
||||
|
||||
// newEngine creates the engine demo queries evaluate on.
|
||||
func newEngine() *promql.Engine {
|
||||
return promql.NewEngine(promql.EngineOpts{
|
||||
MaxSamples: _maxSamples,
|
||||
Timeout: _queryTimeout,
|
||||
EnableAtModifier: true,
|
||||
EnableNegativeOffset: true,
|
||||
NoStepSubqueryIntervalFn: func(int64) int64 {
|
||||
return _tickMillis
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Client answers Prometheus operations out of the demo registry.
|
||||
//
|
||||
// One client serves every query a process answers. The registry it holds
|
||||
// caches the history its walks replay, and a data source runner is built
|
||||
// per request, so a client built per request would throw that cache away
|
||||
// every time.
|
||||
type Client struct {
|
||||
// engine evaluates the queries asked of the registry.
|
||||
engine *promql.Engine
|
||||
|
||||
// parser reads the selectors the label and series endpoints are
|
||||
// narrowed by.
|
||||
parser parser.Parser
|
||||
|
||||
// registry is everything this client publishes.
|
||||
registry registry
|
||||
}
|
||||
|
||||
// NewClient creates a fresh instance of Client.
|
||||
func NewClient() *Client {
|
||||
return &Client{
|
||||
engine: newEngine(),
|
||||
parser: parser.NewParser(parser.Options{}),
|
||||
registry: newRegistry(),
|
||||
}
|
||||
}
|
||||
|
||||
// TestConnection tests the connection to the demo data source, which is
|
||||
// this process and so is always reachable.
|
||||
func (c *Client) TestConnection(context.Context) (processor.ConnectionStatus, error) {
|
||||
return processor.ConnectionStatusSuccess, nil
|
||||
}
|
||||
|
||||
// Metadata retrieves metadata about the demo data source.
|
||||
func (c *Client) Metadata(context.Context) (*processor.PrometheusMetadataResult, error) {
|
||||
md := make(map[string][]v1.Metadata, len(c.registry))
|
||||
|
||||
for _, f := range c.registry {
|
||||
md[f.name] = []v1.Metadata{
|
||||
{
|
||||
Type: v1.MetricTypeGauge,
|
||||
Help: f.help,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &processor.PrometheusMetadataResult{Result: md}, nil
|
||||
}
|
||||
|
||||
// QueryRange performs a query against the demo data source over the
|
||||
// specified time range.
|
||||
func (c *Client) QueryRange(
|
||||
ctx context.Context,
|
||||
q string,
|
||||
tr processor.TimeRange,
|
||||
) (*processor.PrometheusQueryResult, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, _queryTimeout)
|
||||
defer cancel()
|
||||
|
||||
ntr := tr.Normalize()
|
||||
step := ntr.QueryStep()
|
||||
|
||||
// the samples are spaced a step apart, so the lookback window has to
|
||||
// span one to reach the sample behind every point of the grid.
|
||||
query, err := c.engine.NewRangeQuery(
|
||||
ctx,
|
||||
queryable{registry: c.registry},
|
||||
promql.NewPrometheusQueryOpts(false, step+_tick),
|
||||
ntr.ProcessPrometheusQuery(q),
|
||||
ntr.From,
|
||||
ntr.To,
|
||||
step,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, processor.NewInvalidQueryError(err.Error())
|
||||
}
|
||||
|
||||
defer query.Close()
|
||||
|
||||
res := query.Exec(ctx)
|
||||
if res.Err != nil {
|
||||
// an evaluation that ran out of time or was abandoned is not a
|
||||
// query the reader can fix, so it is not reported as one. What
|
||||
// the engine rejects on its own terms — a query too expensive to
|
||||
// run, a type it cannot evaluate — is.
|
||||
if ctx.Err() != nil {
|
||||
return nil, fmt.Errorf("evaluating demo query: %w", res.Err)
|
||||
}
|
||||
|
||||
return nil, processor.NewInvalidQueryError(res.Err.Error())
|
||||
}
|
||||
|
||||
matrix, ok := res.Value.(promql.Matrix)
|
||||
if !ok {
|
||||
// NOCOV: a range query only ever evaluates to a matrix.
|
||||
return nil, errors.New("unexpected demo query result type")
|
||||
}
|
||||
|
||||
warns, _ := res.Warnings.AsStrings(q, 0, 0)
|
||||
|
||||
return &processor.PrometheusQueryResult{
|
||||
Type: model.ValMatrix,
|
||||
Result: toModelMatrix(matrix),
|
||||
Warnings: warns,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LabelNames retrieves the label names the demo's series carry, narrowed
|
||||
// to the ones the matchers select.
|
||||
func (c *Client) LabelNames(
|
||||
_ context.Context,
|
||||
matchers []string,
|
||||
_ processor.TimeRange,
|
||||
) (*processor.PrometheusLabelNamesResult, error) {
|
||||
sets, err := c.parseMatchers(matchers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &processor.PrometheusLabelNamesResult{Result: c.registry.labelNames(sets)}, nil
|
||||
}
|
||||
|
||||
// LabelValues retrieves the values the demo's series carry for the named
|
||||
// label, narrowed to the ones the matchers select.
|
||||
func (c *Client) LabelValues(
|
||||
_ context.Context,
|
||||
label string,
|
||||
matchers []string,
|
||||
_ processor.TimeRange,
|
||||
) (*processor.PrometheusLabelValuesResult, error) {
|
||||
sets, err := c.parseMatchers(matchers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &processor.PrometheusLabelValuesResult{Result: c.registry.labelValues(label, sets)}, nil
|
||||
}
|
||||
|
||||
// Series retrieves the demo's series matching the given selectors.
|
||||
func (c *Client) Series(
|
||||
_ context.Context,
|
||||
matchers []string,
|
||||
_ processor.TimeRange,
|
||||
) (*processor.PrometheusSeriesResult, error) {
|
||||
sets, err := c.parseMatchers(matchers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
res []model.LabelSet
|
||||
seen = map[string]struct{}{}
|
||||
)
|
||||
|
||||
for _, set := range sets {
|
||||
c.registry.forEach(set, func(lbls labels.Labels, _ func(tick int64) float64) {
|
||||
key := lbls.String()
|
||||
|
||||
if _, ok := seen[key]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
seen[key] = struct{}{}
|
||||
|
||||
res = append(res, toModelLabelSet(lbls))
|
||||
})
|
||||
}
|
||||
|
||||
return &processor.PrometheusSeriesResult{Result: res}, nil
|
||||
}
|
||||
|
||||
// parseMatchers turns the API's series selectors into matcher sets. No
|
||||
// selector at all selects everything, which is what the endpoints answer
|
||||
// when the caller narrows nothing.
|
||||
func (c *Client) parseMatchers(mm []string) ([][]*labels.Matcher, error) {
|
||||
if len(mm) == 0 {
|
||||
return [][]*labels.Matcher{nil}, nil
|
||||
}
|
||||
|
||||
sets, err := c.parser.ParseMetricSelectors(mm)
|
||||
if err != nil {
|
||||
return nil, processor.NewInvalidQueryError(err.Error())
|
||||
}
|
||||
|
||||
return sets, nil
|
||||
}
|
||||
|
||||
// toModelMatrix converts an evaluated matrix into the shape the real
|
||||
// Prometheus client returns, so a chart cannot tell the two apart.
|
||||
func toModelMatrix(m promql.Matrix) model.Matrix {
|
||||
res := make(model.Matrix, 0, len(m))
|
||||
|
||||
for _, s := range m {
|
||||
values := make([]model.SamplePair, 0, len(s.Floats))
|
||||
|
||||
for _, p := range s.Floats {
|
||||
values = append(values, model.SamplePair{
|
||||
Timestamp: model.Time(p.T),
|
||||
Value: model.SampleValue(p.F),
|
||||
})
|
||||
}
|
||||
|
||||
res = append(res, &model.SampleStream{
|
||||
Metric: model.Metric(toModelLabelSet(s.Metric)),
|
||||
Values: values,
|
||||
})
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// toModelLabelSet converts an evaluated label set into the shape the real
|
||||
// Prometheus client returns.
|
||||
func toModelLabelSet(lbls labels.Labels) model.LabelSet {
|
||||
ls := make(model.LabelSet, lbls.Len())
|
||||
|
||||
lbls.Range(func(l labels.Label) {
|
||||
ls[model.LabelName(l.Name)] = model.LabelValue(l.Value)
|
||||
})
|
||||
|
||||
return ls
|
||||
}
|
||||
465
server/core/internal/datasource/demo/client_test.go
Normal file
465
server/core/internal/datasource/demo/client_test.go
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
package demo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/oxynote/oxynote/server/core/internal/datasource/processor"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/timeutil"
|
||||
v1 "github.com/prometheus/client_golang/api/prometheus/v1"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// _availableMetricsPath is the welcome document's pool of metric blocks,
|
||||
// whose queries this package has to be able to answer.
|
||||
const _availableMetricsPath = "../../document/files/available_metrics.json"
|
||||
|
||||
// lastThreeHours returns the range a chart asks for by default.
|
||||
func lastThreeHours() processor.TimeRange {
|
||||
now := timeutil.Now()
|
||||
|
||||
return processor.TimeRange{
|
||||
From: now.Add(-3 * time.Hour),
|
||||
To: now,
|
||||
}
|
||||
}
|
||||
|
||||
// resultMatrix returns the matrix a query result carries, failing the
|
||||
// test if it carries anything else.
|
||||
func resultMatrix(t *testing.T, res *processor.PrometheusQueryResult) model.Matrix {
|
||||
t.Helper()
|
||||
|
||||
require.NotNil(t, res)
|
||||
require.Equal(t, model.ValMatrix, res.Type)
|
||||
|
||||
m, ok := res.Result.(model.Matrix)
|
||||
require.True(t, ok, "result is %T, not a matrix", res.Result)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func Test_newEngine(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.NotNil(t, newEngine())
|
||||
}
|
||||
|
||||
func Test_NewClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewClient()
|
||||
require.NotNil(t, c)
|
||||
|
||||
// the client owns what a query needs, so one client answers every
|
||||
// query a process makes and the registry's replay cache survives
|
||||
// between them.
|
||||
assert.NotNil(t, c.engine)
|
||||
assert.NotNil(t, c.parser)
|
||||
assert.Len(t, c.registry, 15)
|
||||
}
|
||||
|
||||
func Test_Client_TestConnection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// the demo is this process, so the connection cannot be down.
|
||||
cs, err := NewClient().TestConnection(context.Background())
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, processor.ConnectionStatusSuccess, cs)
|
||||
}
|
||||
|
||||
func Test_Client_Metadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewClient()
|
||||
|
||||
res, err := c.Metadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, res)
|
||||
|
||||
md, ok := res.Result.(map[string][]v1.Metadata)
|
||||
require.True(t, ok)
|
||||
|
||||
// every family the registry declares is described, as a gauge, with
|
||||
// the help text the editor shows.
|
||||
assert.Len(t, md, 15)
|
||||
|
||||
for _, f := range c.registry {
|
||||
entry, ok := md[f.name]
|
||||
require.True(t, ok, "%s is missing from the metadata", f.name)
|
||||
require.Len(t, entry, 1)
|
||||
|
||||
assert.Equal(t, v1.MetricTypeGauge, entry[0].Type)
|
||||
assert.Equal(t, f.help, entry[0].Help)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Client_QueryRange(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewClient()
|
||||
name := _namespace + "deploy_confidence_index"
|
||||
|
||||
cc := map[string]struct {
|
||||
Query string
|
||||
Range processor.TimeRange
|
||||
Context context.Context //nolint:containedctx // the case states which context the query runs under
|
||||
Series int
|
||||
Err error
|
||||
}{
|
||||
"An abandoned evaluation is not blamed on the query": {
|
||||
Query: name,
|
||||
Range: lastThreeHours(),
|
||||
Context: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
Err: assert.AnError,
|
||||
},
|
||||
"A plain selector returns every series of the family": {
|
||||
Query: name,
|
||||
Range: lastThreeHours(),
|
||||
Series: 3,
|
||||
},
|
||||
"A selector narrowed by a label returns the one series": {
|
||||
Query: name + `{vibe="nervous"}`,
|
||||
Range: lastThreeHours(),
|
||||
Series: 1,
|
||||
},
|
||||
"An aggregation collapses the family": {
|
||||
Query: "sum(" + name + ")",
|
||||
Range: lastThreeHours(),
|
||||
Series: 1,
|
||||
},
|
||||
"A function over a window reads the history behind each point": {
|
||||
Query: "avg_over_time(" + name + "[30m])",
|
||||
Range: lastThreeHours(),
|
||||
Series: 3,
|
||||
},
|
||||
"A macro is expanded before the query runs": {
|
||||
Query: "max_over_time(" + name + "[$__interval])",
|
||||
Range: lastThreeHours(),
|
||||
Series: 3,
|
||||
},
|
||||
"A year of history backfills for free": {
|
||||
Query: name,
|
||||
Range: processor.TimeRange{From: timeutil.Now().AddDate(-1, 0, 0), To: timeutil.Now()},
|
||||
Series: 3,
|
||||
},
|
||||
"A selector naming nothing returns no series": {
|
||||
Query: "not_a_metric",
|
||||
Range: lastThreeHours(),
|
||||
},
|
||||
"A query that does not parse is reported as the user's": {
|
||||
Query: "sum(",
|
||||
Range: lastThreeHours(),
|
||||
Err: assert.AnError,
|
||||
},
|
||||
"A query of the wrong type is reported too": {
|
||||
Query: `"a string"`,
|
||||
Range: lastThreeHours(),
|
||||
Err: assert.AnError,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c2 := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := c2.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
res, err := c.QueryRange(ctx, c2.Query, c2.Range)
|
||||
testutil.AssertEqualError(t, c2.Err, err)
|
||||
|
||||
if c2.Err != nil {
|
||||
assert.Nil(t, res)
|
||||
return
|
||||
}
|
||||
|
||||
m := resultMatrix(t, res)
|
||||
require.Len(t, m, c2.Series)
|
||||
|
||||
ntr := c2.Range.Normalize()
|
||||
|
||||
for _, s := range m {
|
||||
require.NotEmpty(t, s.Values)
|
||||
|
||||
// the line spans the window asked for, with no gap at
|
||||
// either edge, and every point carries a real number.
|
||||
first := s.Values[0].Timestamp.Time()
|
||||
last := s.Values[len(s.Values)-1].Timestamp.Time()
|
||||
|
||||
assert.WithinDuration(t, ntr.From, first, 2*ntr.QueryStep())
|
||||
assert.WithinDuration(t, ntr.To, last, 2*ntr.QueryStep())
|
||||
|
||||
for _, v := range s.Values {
|
||||
assert.False(t, math.IsNaN(float64(v.Value)))
|
||||
assert.False(t, math.IsInf(float64(v.Value), 0))
|
||||
}
|
||||
|
||||
// the point count follows the step the range asks for,
|
||||
// not the range's width in ticks: a year spans 525,600
|
||||
// of them and still draws a few hundred points.
|
||||
assert.Less(t, len(s.Values), 1_000)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Client_QueryRange_isFrozen(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewClient()
|
||||
tr := lastThreeHours()
|
||||
q := _namespace + "stale_branches_total"
|
||||
|
||||
first, err := c.QueryRange(context.Background(), q, tr)
|
||||
require.NoError(t, err)
|
||||
|
||||
second, err := c.QueryRange(context.Background(), q, tr)
|
||||
require.NoError(t, err)
|
||||
|
||||
// the same window answers bit-identically, which is what keeps a
|
||||
// drawn line from rewriting itself under the reader.
|
||||
firstJSON, err := json.Marshal(first.Result)
|
||||
require.NoError(t, err)
|
||||
|
||||
secondJSON, err := json.Marshal(second.Result)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(firstJSON), string(secondJSON))
|
||||
|
||||
// and a window inside it answers the same values for the timestamps
|
||||
// the two share: history does not depend on how it was asked for.
|
||||
inner, err := c.QueryRange(context.Background(), q, processor.TimeRange{
|
||||
From: tr.From.Add(time.Hour),
|
||||
To: tr.To,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
outer := resultMatrix(t, first)
|
||||
require.Len(t, resultMatrix(t, inner), len(outer))
|
||||
}
|
||||
|
||||
func Test_Client_LabelNames(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewClient()
|
||||
|
||||
res, err := c.LabelNames(context.Background(), nil, lastThreeHours())
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, res)
|
||||
|
||||
assert.Contains(t, res.Result, model.MetricNameLabel)
|
||||
assert.Contains(t, res.Result, "vibe")
|
||||
|
||||
// a selector narrows the answer.
|
||||
res, err = c.LabelNames(
|
||||
context.Background(),
|
||||
[]string{_namespace + "deploy_confidence_index"},
|
||||
lastThreeHours(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{model.MetricNameLabel, "vibe"}, res.Result)
|
||||
|
||||
// a selector that does not parse is the caller's mistake.
|
||||
res, err = c.LabelNames(context.Background(), []string{"{{"}, lastThreeHours())
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
}
|
||||
|
||||
func Test_Client_LabelValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewClient()
|
||||
|
||||
// the metric names are what the editor completes on.
|
||||
res, err := c.LabelValues(context.Background(), model.MetricNameLabel, nil, lastThreeHours())
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, res.Result, 15)
|
||||
|
||||
res, err = c.LabelValues(context.Background(), "vibe", nil, lastThreeHours())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"feeling_lucky", "nervous", "oncall_already_paged"}, res.Result)
|
||||
|
||||
res, err = c.LabelValues(
|
||||
context.Background(),
|
||||
"vibe",
|
||||
[]string{_namespace + "secrets_detected_commits_count"},
|
||||
lastThreeHours(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res.Result)
|
||||
|
||||
res, err = c.LabelValues(context.Background(), "vibe", []string{"{{"}, lastThreeHours())
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
}
|
||||
|
||||
func Test_Client_Series(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewClient()
|
||||
|
||||
res, err := c.Series(
|
||||
context.Background(),
|
||||
[]string{_namespace + "deploy_confidence_index"},
|
||||
lastThreeHours(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Result, 3)
|
||||
|
||||
for _, ls := range res.Result {
|
||||
assert.Equal(t, model.LabelValue(_namespace+"deploy_confidence_index"), ls[model.MetricNameLabel])
|
||||
assert.NotEmpty(t, ls["vibe"])
|
||||
}
|
||||
|
||||
// selectors that overlap answer each series once.
|
||||
res, err = c.Series(
|
||||
context.Background(),
|
||||
[]string{
|
||||
_namespace + "deploy_confidence_index",
|
||||
_namespace + `deploy_confidence_index{vibe="nervous"}`,
|
||||
},
|
||||
lastThreeHours(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, res.Result, 3)
|
||||
|
||||
// no selector at all names every series there is.
|
||||
res, err = c.Series(context.Background(), nil, lastThreeHours())
|
||||
require.NoError(t, err)
|
||||
|
||||
var buckets int
|
||||
|
||||
for _, f := range c.registry {
|
||||
buckets += len(f.buckets)
|
||||
}
|
||||
|
||||
assert.Len(t, res.Result, buckets)
|
||||
|
||||
res, err = c.Series(context.Background(), []string{"{{"}, lastThreeHours())
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
}
|
||||
|
||||
func Test_Client_parseMatchers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewClient()
|
||||
|
||||
// no selector means one empty set, which selects everything.
|
||||
sets, err := c.parseMatchers(nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, [][]*labels.Matcher{nil}, sets)
|
||||
|
||||
sets, err = c.parseMatchers([]string{`metric{a="b"}`, "other"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sets, 2)
|
||||
assert.Len(t, sets[0], 2)
|
||||
assert.Len(t, sets[1], 1)
|
||||
|
||||
sets, err = c.parseMatchers([]string{"{{"})
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, sets)
|
||||
}
|
||||
|
||||
func Test_toModelMatrix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := toModelMatrix(promql.Matrix{
|
||||
{
|
||||
Metric: labels.FromStrings(model.MetricNameLabel, "metric", "colour", "red"),
|
||||
Floats: []promql.FPoint{{T: 1_700_000_000_000, F: 1.5}},
|
||||
},
|
||||
})
|
||||
|
||||
require.Len(t, m, 1)
|
||||
assert.Equal(t, model.LabelValue("metric"), m[0].Metric[model.MetricNameLabel])
|
||||
assert.Equal(t, model.LabelValue("red"), m[0].Metric["colour"])
|
||||
|
||||
require.Len(t, m[0].Values, 1)
|
||||
assert.Equal(t, model.SampleValue(1.5), m[0].Values[0].Value)
|
||||
assert.Equal(t, int64(1_700_000_000), m[0].Values[0].Timestamp.Unix())
|
||||
|
||||
assert.Empty(t, toModelMatrix(nil))
|
||||
}
|
||||
|
||||
func Test_toModelLabelSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ls := toModelLabelSet(labels.FromStrings(model.MetricNameLabel, "metric", "colour", "red"))
|
||||
|
||||
assert.Equal(t, model.LabelSet{
|
||||
model.MetricNameLabel: "metric",
|
||||
"colour": "red",
|
||||
}, ls)
|
||||
|
||||
assert.Empty(t, toModelLabelSet(labels.EmptyLabels()))
|
||||
}
|
||||
|
||||
// Test_welcomeDocumentQueries guards the pair that has no compiler
|
||||
// between them: the welcome document's metric blocks name metrics by
|
||||
// string, and this package is what has to answer them. A family renamed
|
||||
// on one side and not the other empties the document's charts, which
|
||||
// nothing else here would notice.
|
||||
func Test_welcomeDocumentQueries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
raw, err := os.ReadFile(_availableMetricsPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
var blocks []struct {
|
||||
Attrs struct {
|
||||
Queries []struct {
|
||||
Query string `json:"query"`
|
||||
} `json:"queries"`
|
||||
} `json:"attrs"`
|
||||
}
|
||||
|
||||
require.NoError(t, json.Unmarshal(raw, &blocks))
|
||||
require.NotEmpty(t, blocks)
|
||||
|
||||
c := NewClient()
|
||||
tr := lastThreeHours()
|
||||
|
||||
var queries int
|
||||
|
||||
for _, b := range blocks {
|
||||
for _, q := range b.Attrs.Queries {
|
||||
queries++
|
||||
|
||||
t.Run(q.Query, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
res, err := c.QueryRange(context.Background(), q.Query, tr)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := resultMatrix(t, res)
|
||||
assert.NotEmpty(t, m, "%s returned no series", q.Query)
|
||||
|
||||
for _, s := range m {
|
||||
assert.NotEmpty(t, s.Values, "%s returned an empty series", q.Query)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// every family the registry declares earns its place in the pool the
|
||||
// document draws from, and vice versa.
|
||||
assert.Len(t, c.registry, queries)
|
||||
}
|
||||
28
server/core/internal/datasource/demo/demo.go
Normal file
28
server/core/internal/datasource/demo/demo.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Package demo serves a self-contained Prometheus data source: its metrics
|
||||
// are synthesized on demand from a fixed seed instead of being scraped from
|
||||
// a running server, so a fresh install shows populated charts with nothing
|
||||
// else deployed.
|
||||
package demo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/oxynote/oxynote/server/core/pkg/errutil"
|
||||
)
|
||||
|
||||
const (
|
||||
// Scheme prefixes every URL this package answers for.
|
||||
Scheme = "demo://"
|
||||
|
||||
// URL is the only demo data source that exists.
|
||||
URL = Scheme + "engineering"
|
||||
)
|
||||
|
||||
// ErrUnknownSource is returned when a demo URL names a source that does
|
||||
// not exist.
|
||||
var ErrUnknownSource = errutil.New(
|
||||
http.StatusBadRequest,
|
||||
"data_source.unknown_demo_source",
|
||||
"Unknown demo data source. The only demo data source is %q.",
|
||||
URL,
|
||||
)
|
||||
31
server/core/internal/datasource/demo/demo_test.go
Normal file
31
server/core/internal/datasource/demo/demo_test.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package demo
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
func Test_URL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// the runner routes on the scheme before it routes on the whole URL,
|
||||
// so the one demo source has to carry it.
|
||||
assert.Equal(t, "demo://", Scheme)
|
||||
assert.Equal(t, "demo://engineering", URL)
|
||||
}
|
||||
|
||||
func Test_ErrUnknownSource(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Error(t, ErrUnknownSource)
|
||||
|
||||
// the message names the source that does exist: the reader mistyped
|
||||
// something, and the only useful answer is what they meant.
|
||||
assert.Contains(t, ErrUnknownSource.Error(), URL)
|
||||
}
|
||||
245
server/core/internal/datasource/demo/queryable.go
Normal file
245
server/core/internal/datasource/demo/queryable.go
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
package demo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/prometheus/prometheus/model/histogram"
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/storage"
|
||||
"github.com/prometheus/prometheus/tsdb/chunkenc"
|
||||
"github.com/prometheus/prometheus/tsdb/chunks"
|
||||
"github.com/prometheus/prometheus/util/annotations"
|
||||
)
|
||||
|
||||
// _samplesPerRange is the least number of samples a range selector's
|
||||
// window holds, so a function reading a window has something to read.
|
||||
const _samplesPerRange = 4
|
||||
|
||||
// _maxSamplesPerSeries caps what one series contributes to a single
|
||||
// evaluation. Reaching it widens the spacing rather than cutting the
|
||||
// series short: a coarser line still spans the window that was asked
|
||||
// for, while a truncated one would simply stop mid-chart.
|
||||
//
|
||||
// It has to stay well above the hundred-odd points a time range resolves
|
||||
// its step to. Widening spaces the samples span/_maxSamplesPerSeries
|
||||
// apart, and the lookback window a query opens is its own step wide — so
|
||||
// a cap below that ratio would space samples further apart than the
|
||||
// window that has to find them, and points of the grid would read empty.
|
||||
const _maxSamplesPerSeries = 5_000
|
||||
|
||||
// selectStep reports how far apart the samples of one Select sit. The
|
||||
// engine reads on a fixed grid and every point of it has to find a sample
|
||||
// behind it, so the step the query evaluates at is also the coarsest the
|
||||
// samples may be spaced; a range selector needs several samples inside
|
||||
// every window on top of that.
|
||||
func selectStep(hints *storage.SelectHints, span int64) int64 {
|
||||
step := _tickMillis
|
||||
|
||||
if hints != nil {
|
||||
if hints.Step > step {
|
||||
step = hints.Step
|
||||
}
|
||||
|
||||
if hints.Range > 0 {
|
||||
if within := hints.Range / _samplesPerRange; within < step {
|
||||
step = within
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if step < _tickMillis {
|
||||
step = _tickMillis
|
||||
}
|
||||
|
||||
if widest := span / _maxSamplesPerSeries; widest > step {
|
||||
step = widest
|
||||
}
|
||||
|
||||
return step
|
||||
}
|
||||
|
||||
// samplesIn returns one series' readings across the given millisecond
|
||||
// window, spaced a step apart and aligned to absolute multiples of it so
|
||||
// the same window always yields the same timestamps. The first sample
|
||||
// sits at or before the window's start, which is what gives the first
|
||||
// point of the evaluation grid something to look back at.
|
||||
func samplesIn(value func(tick int64) float64, start, end, step int64) []chunks.Sample {
|
||||
if last := timeAt(latestTick()).UnixMilli(); end > last {
|
||||
end = last
|
||||
}
|
||||
|
||||
var ss []chunks.Sample
|
||||
|
||||
for ts := start / step * step; ts <= end; ts += step {
|
||||
tick := tickAtMillis(ts)
|
||||
if tick < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
ss = append(ss, floatSample{t: ts, f: value(tick)})
|
||||
}
|
||||
|
||||
return ss
|
||||
}
|
||||
|
||||
// queryable serves one client's registry to the promql engine.
|
||||
type queryable struct {
|
||||
// registry is what the evaluation reads out of.
|
||||
registry registry
|
||||
}
|
||||
|
||||
// Querier returns a querier scoped to the given millisecond bounds.
|
||||
func (q queryable) Querier(mint, maxt int64) (storage.Querier, error) {
|
||||
return &querier{registry: q.registry, mint: mint, maxt: maxt}, nil
|
||||
}
|
||||
|
||||
// querier answers one promql evaluation out of the registry.
|
||||
type querier struct {
|
||||
// registry is what the evaluation reads out of.
|
||||
registry registry
|
||||
|
||||
// mint and maxt are the inclusive millisecond bounds the evaluation
|
||||
// reads within.
|
||||
mint int64
|
||||
maxt int64
|
||||
}
|
||||
|
||||
// Select returns the series the matchers name, sampled across the window
|
||||
// the hints ask about.
|
||||
func (q *querier) Select(
|
||||
_ context.Context,
|
||||
sortSeries bool,
|
||||
hints *storage.SelectHints,
|
||||
mm ...*labels.Matcher,
|
||||
) storage.SeriesSet {
|
||||
start, end := q.mint, q.maxt
|
||||
|
||||
if hints != nil {
|
||||
start, end = hints.Start, hints.End
|
||||
}
|
||||
|
||||
step := selectStep(hints, end-start)
|
||||
|
||||
var ss []storage.Series
|
||||
|
||||
q.registry.forEach(mm, func(lbls labels.Labels, value func(tick int64) float64) {
|
||||
ss = append(ss, storage.NewListSeries(lbls, samplesIn(value, start, end, step)))
|
||||
})
|
||||
|
||||
if sortSeries {
|
||||
slices.SortFunc(ss, func(a, b storage.Series) int {
|
||||
return labels.Compare(a.Labels(), b.Labels())
|
||||
})
|
||||
}
|
||||
|
||||
return &seriesSet{series: ss, index: -1}
|
||||
}
|
||||
|
||||
// LabelNames returns every label name the matching series carry, sorted.
|
||||
func (q *querier) LabelNames(
|
||||
_ context.Context,
|
||||
_ *storage.LabelHints,
|
||||
mm ...*labels.Matcher,
|
||||
) ([]string, annotations.Annotations, error) {
|
||||
return q.registry.labelNames([][]*labels.Matcher{mm}), nil, nil
|
||||
}
|
||||
|
||||
// LabelValues returns every value the matching series carry for the named
|
||||
// label, sorted.
|
||||
func (q *querier) LabelValues(
|
||||
_ context.Context,
|
||||
name string,
|
||||
_ *storage.LabelHints,
|
||||
mm ...*labels.Matcher,
|
||||
) ([]string, annotations.Annotations, error) {
|
||||
return q.registry.labelValues(name, [][]*labels.Matcher{mm}), nil, nil
|
||||
}
|
||||
|
||||
// Close releases the querier's resources, of which it holds none.
|
||||
func (q *querier) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// seriesSet iterates the series one Select produced.
|
||||
type seriesSet struct {
|
||||
// series are the selected series, already sorted when the caller
|
||||
// asked for that.
|
||||
series []storage.Series
|
||||
|
||||
// index is the position At reports, before Next has run for the
|
||||
// first time -1.
|
||||
index int
|
||||
}
|
||||
|
||||
// Next advances to the next series and reports whether one is there.
|
||||
func (ss *seriesSet) Next() bool {
|
||||
ss.index++
|
||||
|
||||
return ss.index < len(ss.series)
|
||||
}
|
||||
|
||||
// At returns the series the iteration currently sits on.
|
||||
func (ss *seriesSet) At() storage.Series {
|
||||
return ss.series[ss.index]
|
||||
}
|
||||
|
||||
// Err returns the error iteration failed with, of which synthesized data
|
||||
// has none.
|
||||
func (ss *seriesSet) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Warnings returns the warnings gathered over the set, of which
|
||||
// synthesized data has none.
|
||||
func (ss *seriesSet) Warnings() annotations.Annotations {
|
||||
return nil
|
||||
}
|
||||
|
||||
// floatSample is one demo reading in the shape the storage layer reads
|
||||
// samples in. The demo publishes gauges only, so the histogram accessors
|
||||
// report none.
|
||||
type floatSample struct {
|
||||
// t is the sample's millisecond timestamp.
|
||||
t int64
|
||||
|
||||
// f is the value read at that instant.
|
||||
f float64
|
||||
}
|
||||
|
||||
// T returns the sample's timestamp in milliseconds.
|
||||
func (s floatSample) T() int64 {
|
||||
return s.t
|
||||
}
|
||||
|
||||
// ST returns the sample's start timestamp, which a synthesized reading
|
||||
// does not carry.
|
||||
func (s floatSample) ST() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// F returns the sample's value.
|
||||
func (s floatSample) F() float64 {
|
||||
return s.f
|
||||
}
|
||||
|
||||
// H returns the sample's histogram, of which a gauge has none.
|
||||
func (s floatSample) H() *histogram.Histogram {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FH returns the sample's float histogram, of which a gauge has none.
|
||||
func (s floatSample) FH() *histogram.FloatHistogram {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type reports what kind of value the sample carries.
|
||||
func (s floatSample) Type() chunkenc.ValueType {
|
||||
return chunkenc.ValFloat
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of the sample, which holds no references and
|
||||
// so is itself.
|
||||
func (s floatSample) Copy() chunks.Sample {
|
||||
return s
|
||||
}
|
||||
279
server/core/internal/datasource/demo/queryable_test.go
Normal file
279
server/core/internal/datasource/demo/queryable_test.go
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
package demo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/storage"
|
||||
"github.com/prometheus/prometheus/tsdb/chunkenc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// countingValue returns a value function reporting the tick it was asked
|
||||
// about, so a caller can tell which tick produced a sample.
|
||||
func countingValue(tick int64) float64 {
|
||||
return float64(tick)
|
||||
}
|
||||
|
||||
func Test_selectStep(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
Hints *storage.SelectHints
|
||||
Span int64
|
||||
Result int64
|
||||
}{
|
||||
"No hints at all falls back to the tick": {
|
||||
Result: _tickMillis,
|
||||
},
|
||||
"A step finer than the tick is widened to it": {
|
||||
Hints: &storage.SelectHints{Step: 15_000},
|
||||
Result: _tickMillis,
|
||||
},
|
||||
"A step coarser than the tick is kept": {
|
||||
Hints: &storage.SelectHints{Step: 5 * _tickMillis},
|
||||
Result: 5 * _tickMillis,
|
||||
},
|
||||
"A range selector is sampled several times per window": {
|
||||
Hints: &storage.SelectHints{Step: 100 * _tickMillis, Range: 20 * _tickMillis},
|
||||
Result: 5 * _tickMillis,
|
||||
},
|
||||
"A range shorter than the tick still samples at the tick": {
|
||||
Hints: &storage.SelectHints{Step: 100 * _tickMillis, Range: 1_000},
|
||||
Result: _tickMillis,
|
||||
},
|
||||
"A range wider than the step leaves the step alone": {
|
||||
Hints: &storage.SelectHints{Step: 2 * _tickMillis, Range: 1_000 * _tickMillis},
|
||||
Result: 2 * _tickMillis,
|
||||
},
|
||||
"A span too wide for the cap widens the spacing": {
|
||||
Hints: &storage.SelectHints{Step: _tickMillis},
|
||||
Span: _tickMillis * _maxSamplesPerSeries * 3,
|
||||
Result: _tickMillis * 3,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
step := selectStep(c.Hints, c.Span)
|
||||
assert.Equal(t, c.Result, step)
|
||||
|
||||
// whatever the inputs, one series never outgrows the cap and
|
||||
// never samples finer than the timeline itself.
|
||||
assert.GreaterOrEqual(t, step, _tickMillis)
|
||||
assert.LessOrEqual(t, c.Span/step, int64(_maxSamplesPerSeries))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_samplesIn(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
start := _epoch.Add(time.Hour).UnixMilli()
|
||||
end := _epoch.Add(2 * time.Hour).UnixMilli()
|
||||
|
||||
ss := samplesIn(countingValue, start, end, _tickMillis)
|
||||
|
||||
require.NotEmpty(t, ss)
|
||||
|
||||
// the window is covered end to end, one sample a step apart, and
|
||||
// each carries the value of the tick it lands in.
|
||||
assert.Len(t, ss, 61)
|
||||
assert.Equal(t, start, ss[0].T())
|
||||
assert.Equal(t, end, ss[len(ss)-1].T())
|
||||
|
||||
for i, s := range ss {
|
||||
assert.Equal(t, float64(tickAtMillis(s.T())), s.F())
|
||||
|
||||
if i > 0 {
|
||||
assert.Equal(t, _tickMillis, s.T()-ss[i-1].T())
|
||||
}
|
||||
}
|
||||
|
||||
// the first sample sits at or before the window's start, so the
|
||||
// first point of the evaluation grid has something to look back at.
|
||||
ss = samplesIn(countingValue, start+30_000, end, _tickMillis)
|
||||
require.NotEmpty(t, ss)
|
||||
assert.LessOrEqual(t, ss[0].T(), start+30_000)
|
||||
|
||||
// nothing past the present is ever sampled.
|
||||
last := timeAt(latestTick()).UnixMilli()
|
||||
|
||||
ss = samplesIn(countingValue, last-10*_tickMillis, last+time.Hour.Milliseconds(), _tickMillis)
|
||||
require.NotEmpty(t, ss)
|
||||
assert.LessOrEqual(t, ss[len(ss)-1].T(), last)
|
||||
|
||||
// a window that lands entirely before the epoch produces nothing
|
||||
// rather than running the timeline backwards.
|
||||
assert.Empty(t, samplesIn(
|
||||
countingValue,
|
||||
_epoch.Add(-10*time.Hour).UnixMilli(),
|
||||
_epoch.Add(-5*time.Hour).UnixMilli(),
|
||||
_tickMillis,
|
||||
))
|
||||
}
|
||||
|
||||
func Test_queryable_Querier(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
q, err := queryable{}.Querier(10, 20)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, q)
|
||||
|
||||
qr, ok := q.(*querier)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, int64(10), qr.mint)
|
||||
assert.Equal(t, int64(20), qr.maxt)
|
||||
}
|
||||
|
||||
func Test_querier_Select(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
start = _epoch.Add(time.Hour).UnixMilli()
|
||||
end = _epoch.Add(2 * time.Hour).UnixMilli()
|
||||
name = _namespace + "deploy_confidence_index"
|
||||
)
|
||||
|
||||
q := &querier{registry: newRegistry(), mint: start, maxt: end}
|
||||
|
||||
// the matchers narrow the set, and every series in it carries the
|
||||
// samples of the window the hints asked about.
|
||||
set := q.Select(
|
||||
context.Background(),
|
||||
true,
|
||||
&storage.SelectHints{Start: start, End: end, Step: _tickMillis},
|
||||
nameMatcher(t, name),
|
||||
)
|
||||
|
||||
require.NoError(t, set.Err())
|
||||
assert.Empty(t, set.Warnings())
|
||||
|
||||
var got []string
|
||||
|
||||
for set.Next() {
|
||||
s := set.At()
|
||||
got = append(got, s.Labels().Get("vibe"))
|
||||
|
||||
assert.Equal(t, name, s.Labels().Get(model.MetricNameLabel))
|
||||
|
||||
var samples int
|
||||
|
||||
it := s.Iterator(nil)
|
||||
for it.Next() == chunkenc.ValFloat {
|
||||
ts, _ := it.At()
|
||||
|
||||
assert.GreaterOrEqual(t, ts, start)
|
||||
assert.LessOrEqual(t, ts, end)
|
||||
|
||||
samples++
|
||||
}
|
||||
|
||||
require.NoError(t, it.Err())
|
||||
assert.Equal(t, 61, samples)
|
||||
}
|
||||
|
||||
// sorting was asked for, so the series arrive in label order.
|
||||
assert.Equal(t, []string{"feeling_lucky", "nervous", "oncall_already_paged"}, got)
|
||||
|
||||
// with no hints the querier falls back to its own bounds rather than
|
||||
// answering with nothing.
|
||||
set = q.Select(context.Background(), false, nil, nameMatcher(t, name))
|
||||
|
||||
require.True(t, set.Next())
|
||||
assert.NotNil(t, set.At())
|
||||
|
||||
// a matcher naming nothing yields an empty set, not an error.
|
||||
set = q.Select(context.Background(), false, nil, nameMatcher(t, "nope"))
|
||||
|
||||
assert.False(t, set.Next())
|
||||
require.NoError(t, set.Err())
|
||||
}
|
||||
|
||||
func Test_querier_LabelNames(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
names, annos, err := (&querier{registry: newRegistry()}).LabelNames(
|
||||
context.Background(),
|
||||
nil,
|
||||
nameMatcher(t, _namespace+"deploy_confidence_index"),
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, annos)
|
||||
assert.Equal(t, []string{model.MetricNameLabel, "vibe"}, names)
|
||||
}
|
||||
|
||||
func Test_querier_LabelValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
values, annos, err := (&querier{registry: newRegistry()}).LabelValues(
|
||||
context.Background(),
|
||||
"vibe",
|
||||
nil,
|
||||
nameMatcher(t, _namespace+"deploy_confidence_index"),
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, annos)
|
||||
assert.Equal(t, []string{"feeling_lucky", "nervous", "oncall_already_paged"}, values)
|
||||
}
|
||||
|
||||
func Test_querier_Close(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.NoError(t, (&querier{}).Close())
|
||||
}
|
||||
|
||||
func Test_seriesSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
one := storage.NewListSeries(labels.FromStrings(model.MetricNameLabel, "one"), nil)
|
||||
two := storage.NewListSeries(labels.FromStrings(model.MetricNameLabel, "two"), nil)
|
||||
|
||||
ss := &seriesSet{series: []storage.Series{one, two}, index: -1}
|
||||
|
||||
require.True(t, ss.Next())
|
||||
assert.Same(t, one, ss.At())
|
||||
|
||||
require.True(t, ss.Next())
|
||||
assert.Same(t, two, ss.At())
|
||||
|
||||
// the set ends, and stays ended.
|
||||
assert.False(t, ss.Next())
|
||||
assert.False(t, ss.Next())
|
||||
|
||||
assert.NoError(t, ss.Err())
|
||||
assert.Empty(t, ss.Warnings())
|
||||
|
||||
// an empty set reports nothing rather than panicking on the first
|
||||
// read.
|
||||
empty := &seriesSet{index: -1}
|
||||
assert.False(t, empty.Next())
|
||||
}
|
||||
|
||||
func Test_floatSample(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := floatSample{t: 1_700_000_000_000, f: 42.5}
|
||||
|
||||
assert.Equal(t, int64(1_700_000_000_000), s.T())
|
||||
assert.Equal(t, 42.5, s.F())
|
||||
assert.Equal(t, chunkenc.ValFloat, s.Type())
|
||||
|
||||
// a synthesized gauge reading carries no start timestamp and no
|
||||
// histogram of either shape.
|
||||
assert.Zero(t, s.ST())
|
||||
assert.Nil(t, s.H())
|
||||
assert.Nil(t, s.FH())
|
||||
|
||||
// it holds no references, so a copy is the value itself.
|
||||
assert.Equal(t, s, s.Copy())
|
||||
}
|
||||
556
server/core/internal/datasource/demo/registry.go
Normal file
556
server/core/internal/datasource/demo/registry.go
Normal file
|
|
@ -0,0 +1,556 @@
|
|||
package demo
|
||||
|
||||
import (
|
||||
"math"
|
||||
"slices"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
)
|
||||
|
||||
// _namespace prefixes every demo metric name.
|
||||
const _namespace = "engineering_"
|
||||
|
||||
// _labelOutcome names the label both outcome-split families are broken
|
||||
// down by.
|
||||
const _labelOutcome = "outcome"
|
||||
|
||||
// Sampling parameters of the friday_deploys family, whose two buckets are
|
||||
// derived from one another instead of being sampled independently.
|
||||
const (
|
||||
// _fridayDeployMean is the weekly number of Friday deploys attempted.
|
||||
_fridayDeployMean = 30_000
|
||||
|
||||
// _fridayDeployRelStdDev spreads that count tick to tick.
|
||||
_fridayDeployRelStdDev = 0.15
|
||||
|
||||
// _fridayDeploySuccessMean is the mean fraction of attempts that
|
||||
// survive.
|
||||
_fridayDeploySuccessMean = 0.68
|
||||
|
||||
// _fridayDeploySuccessStdDev spreads that fraction tick to tick.
|
||||
_fridayDeploySuccessStdDev = 0.12
|
||||
)
|
||||
|
||||
// bucket is one series of a metric family.
|
||||
type bucket struct {
|
||||
// label is the label value the bucket is published under. It is
|
||||
// empty for a family that publishes a single unlabelled series.
|
||||
label string
|
||||
|
||||
// value reports the bucket's reading at the given tick.
|
||||
value func(tick int64) float64
|
||||
}
|
||||
|
||||
// family is one metric family of the demo timeline.
|
||||
type family struct {
|
||||
// name is the metric name the family is published under.
|
||||
name string
|
||||
|
||||
// help describes what the family measures.
|
||||
help string
|
||||
|
||||
// label is the label key the family's buckets are broken down by.
|
||||
// It is empty for a family that publishes a single unlabelled
|
||||
// series.
|
||||
label string
|
||||
|
||||
// buckets are the family's series, one per label value.
|
||||
buckets []bucket
|
||||
}
|
||||
|
||||
// registry is everything the demo publishes. One is built per client and
|
||||
// read by every query it answers: the walks behind it cache the history
|
||||
// they replay, so a registry that is rebuilt is a cache that is thrown
|
||||
// away.
|
||||
type registry []family
|
||||
|
||||
// forEach calls fn for every series the matchers select, in declaration
|
||||
// order.
|
||||
func (r registry) forEach(mm []*labels.Matcher, fn func(lbls labels.Labels, value func(tick int64) float64)) {
|
||||
for _, f := range r {
|
||||
for _, b := range f.buckets {
|
||||
lbls := seriesLabels(f, b)
|
||||
|
||||
if matches(lbls, mm) {
|
||||
fn(lbls, b.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// labelNames returns every label name the given matcher sets select,
|
||||
// sorted. The sets are unioned, the way a series endpoint handed several
|
||||
// selectors answers with everything any one of them names.
|
||||
func (r registry) labelNames(sets [][]*labels.Matcher) []string {
|
||||
names := map[string]struct{}{}
|
||||
|
||||
for _, set := range sets {
|
||||
r.forEach(set, func(lbls labels.Labels, _ func(tick int64) float64) {
|
||||
lbls.Range(func(l labels.Label) {
|
||||
names[l.Name] = struct{}{}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return sortedKeys(names)
|
||||
}
|
||||
|
||||
// labelValues returns every value the given matcher sets carry for the
|
||||
// named label, sorted and unioned across the sets.
|
||||
func (r registry) labelValues(name string, sets [][]*labels.Matcher) []string {
|
||||
values := map[string]struct{}{}
|
||||
|
||||
for _, set := range sets {
|
||||
r.forEach(set, func(lbls labels.Labels, _ func(tick int64) float64) {
|
||||
if v := lbls.Get(name); v != "" {
|
||||
values[v] = struct{}{}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return sortedKeys(values)
|
||||
}
|
||||
|
||||
// seriesLabels returns the label set the given bucket of the given family
|
||||
// is published under.
|
||||
func seriesLabels(f family, b bucket) labels.Labels {
|
||||
if f.label == "" {
|
||||
return labels.FromStrings(model.MetricNameLabel, f.name)
|
||||
}
|
||||
|
||||
return labels.FromStrings(model.MetricNameLabel, f.name, f.label, b.label)
|
||||
}
|
||||
|
||||
// matches reports whether the label set satisfies every matcher.
|
||||
func matches(lbls labels.Labels, mm []*labels.Matcher) bool {
|
||||
for _, m := range mm {
|
||||
if !m.Matches(lbls.Get(m.Name)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// sortedKeys returns the set's members in sorted order, and an empty
|
||||
// slice when it holds none.
|
||||
func sortedKeys(set map[string]struct{}) []string {
|
||||
kk := make([]string, 0, len(set))
|
||||
|
||||
for k := range set {
|
||||
kk = append(kk, k)
|
||||
}
|
||||
|
||||
slices.Sort(kk)
|
||||
|
||||
return kk
|
||||
}
|
||||
|
||||
// builder assigns every series its own random stream as the registry is
|
||||
// declared, so the table below carries tuned parameters rather than
|
||||
// hand-written seeds, and no two series ever draw the same numbers.
|
||||
type builder struct {
|
||||
// seed is the last seed handed out.
|
||||
seed int64
|
||||
}
|
||||
|
||||
// next hands out the next unused seed.
|
||||
func (b *builder) next() int64 {
|
||||
b.seed++
|
||||
|
||||
return b.seed
|
||||
}
|
||||
|
||||
// count declares a bucket resampled from scratch every tick.
|
||||
func (b *builder) count(label string, mean, relStdDev float64) bucket {
|
||||
seed := b.next()
|
||||
|
||||
return bucket{
|
||||
label: label,
|
||||
value: func(tick int64) float64 {
|
||||
return countAt(seed, tick, mean, relStdDev)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// walk declares a bucket that drifts between ticks.
|
||||
func (b *builder) walk(label string, p walkParams) bucket {
|
||||
w := newWalk(b.next(), p)
|
||||
|
||||
return bucket{
|
||||
label: label,
|
||||
value: w.at,
|
||||
}
|
||||
}
|
||||
|
||||
// deploys declares the friday_deploys buckets. The successful bucket is a
|
||||
// fraction of the attempted one rather than an independent sample, so a
|
||||
// chart of the two never shows more deploys surviving than were tried.
|
||||
func (b *builder) deploys() []bucket {
|
||||
attempted := b.count("attempted", _fridayDeployMean, _fridayDeployRelStdDev)
|
||||
seed := b.next()
|
||||
|
||||
return []bucket{
|
||||
attempted,
|
||||
{
|
||||
label: "successful",
|
||||
value: func(tick int64) float64 {
|
||||
rate := clamp(
|
||||
normal(newRand(seed, tick), _fridayDeploySuccessMean, _fridayDeploySuccessStdDev),
|
||||
0,
|
||||
1,
|
||||
)
|
||||
|
||||
return math.Round(attempted.value(tick) * rate)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// newRegistry declares the demo's metric families. Means are aggregate
|
||||
// weekly totals across the ~10k organisations the demo pretends to
|
||||
// measure.
|
||||
//
|
||||
//nolint:funlen,maintidx // a flat table of tuned parameters, not logic
|
||||
func newRegistry() registry {
|
||||
b := &builder{}
|
||||
|
||||
return registry{
|
||||
{
|
||||
name: _namespace + "meetings_could_be_doc_count",
|
||||
help: "Weekly count of meetings that could've been a document.",
|
||||
label: "excuse",
|
||||
buckets: []bucket{
|
||||
b.count("alignment", 42_000, 0.15),
|
||||
b.count("bandwidth", 36_000, 0.15),
|
||||
b.count("synergy", 30_000, 0.15),
|
||||
b.count("optics", 12_000, 0.20),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: _namespace + "circle_back_count",
|
||||
help: `Weekly count of "let's circle back" mentions.`,
|
||||
label: _labelOutcome,
|
||||
buckets: []bucket{
|
||||
b.count("circled_back", 16_000, 0.20),
|
||||
b.count("forgotten", 48_000, 0.15),
|
||||
b.count("rescheduled_to_circle_back", 16_000, 0.20),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: _namespace + "friday_deploys_count",
|
||||
help: "Weekly Friday deploy counts.",
|
||||
label: _labelOutcome,
|
||||
buckets: b.deploys(),
|
||||
},
|
||||
{
|
||||
name: _namespace + "pizza_fridays_next_30_days",
|
||||
help: "Pizza Fridays scheduled in the next 30 days.",
|
||||
label: "pizza_count",
|
||||
buckets: []bucket{
|
||||
b.count("1_to_5", 10_000, 0.15),
|
||||
b.count("5_to_10", 7_000, 0.20),
|
||||
b.count("10_plus", 3_000, 0.25),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: _namespace + "ship_it_approvals_after_5pm_count",
|
||||
help: `Weekly count of "ship it" PR approvals left after 5 pm.`,
|
||||
label: "reviewer_state",
|
||||
buckets: []bucket{
|
||||
b.count("sober", 20_000, 0.15),
|
||||
b.count("one_beer_in", 14_000, 0.20),
|
||||
b.count("heading_to_the_door", 6_000, 0.25),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: _namespace + "prs_merged_friday_afternoon_count",
|
||||
help: "Weekly count of PRs merged on Fridays after 3 pm.",
|
||||
label: "confidence",
|
||||
buckets: []bucket{
|
||||
b.count("yolo", 20_000, 0.15),
|
||||
b.count("fingers_crossed", 22_500, 0.15),
|
||||
b.count("vacation_starts_monday", 7_500, 0.20),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: _namespace + "secrets_detected_commits_count",
|
||||
help: "Secrets detected in commits over the past week.",
|
||||
label: "severity",
|
||||
buckets: []bucket{
|
||||
b.count("normal", 2_100, 0.25),
|
||||
b.count("critical", 750, 0.30),
|
||||
b.count("nuclear", 150, 0.35),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: _namespace + "quick_sync_meetings_same_day_count",
|
||||
help: `Weekly count of "quick sync" meetings scheduled same-day.`,
|
||||
label: "duration",
|
||||
buckets: []bucket{
|
||||
b.count("10m_to_30m", 33_000, 0.15),
|
||||
b.count("30m_to_1h", 21_000, 0.15),
|
||||
b.count("1h_plus", 6_000, 0.25),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: _namespace + "temporary_fix_age_days",
|
||||
help: `Age in days of the oldest surviving "temporary" fix.`,
|
||||
label: "original_comment",
|
||||
buckets: []bucket{
|
||||
b.walk("temp", walkParams{
|
||||
Min: 365,
|
||||
Max: 5_000,
|
||||
Start: 2_000,
|
||||
Target: 2_200,
|
||||
MeanReversion: 0.015,
|
||||
NoiseStdDev: 80,
|
||||
DriftPerStep: 0.2,
|
||||
SpikeChance: 0.08,
|
||||
SpikeStdDev: 200,
|
||||
}),
|
||||
b.walk("hack", walkParams{
|
||||
Min: 1_000,
|
||||
Max: 18_250,
|
||||
Start: 6_000,
|
||||
Target: 7_000,
|
||||
MeanReversion: 0.008,
|
||||
NoiseStdDev: 150,
|
||||
DriftPerStep: 0.4,
|
||||
SpikeChance: 0.07,
|
||||
SpikeStdDev: 400,
|
||||
}),
|
||||
b.walk("fixme", walkParams{
|
||||
Min: 180,
|
||||
Max: 4_000,
|
||||
Start: 1_500,
|
||||
Target: 1_700,
|
||||
MeanReversion: 0.015,
|
||||
NoiseStdDev: 60,
|
||||
DriftPerStep: 0.2,
|
||||
SpikeChance: 0.08,
|
||||
SpikeStdDev: 150,
|
||||
}),
|
||||
b.walk("todo", walkParams{
|
||||
Min: 500,
|
||||
Max: 10_000,
|
||||
Start: 3_500,
|
||||
Target: 4_000,
|
||||
MeanReversion: 0.008,
|
||||
NoiseStdDev: 100,
|
||||
DriftPerStep: 0.3,
|
||||
SpikeChance: 0.08,
|
||||
SpikeStdDev: 250,
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: _namespace + "deploy_confidence_index",
|
||||
help: "Self-reported deploy confidence index (0–100).",
|
||||
label: "vibe",
|
||||
buckets: []bucket{
|
||||
b.walk("feeling_lucky", walkParams{
|
||||
Min: 60,
|
||||
Max: 100,
|
||||
Start: 78,
|
||||
Target: 80,
|
||||
MeanReversion: 0.08,
|
||||
NoiseStdDev: 3,
|
||||
SpikeChance: 0.05,
|
||||
SpikeStdDev: 8,
|
||||
}),
|
||||
b.walk("nervous", walkParams{
|
||||
Min: 30,
|
||||
Max: 75,
|
||||
Start: 52,
|
||||
Target: 55,
|
||||
MeanReversion: 0.08,
|
||||
NoiseStdDev: 4,
|
||||
SpikeChance: 0.07,
|
||||
SpikeStdDev: 10,
|
||||
}),
|
||||
b.walk("oncall_already_paged", walkParams{
|
||||
Min: 0,
|
||||
Max: 40,
|
||||
Start: 18,
|
||||
Target: 20,
|
||||
MeanReversion: 0.1,
|
||||
NoiseStdDev: 3,
|
||||
SpikeChance: 0.1,
|
||||
SpikeStdDev: 8,
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: _namespace + "pr_time_to_first_review_minutes",
|
||||
help: "Median time from PR open to first review comment (minutes).",
|
||||
buckets: []bucket{
|
||||
b.walk("", walkParams{
|
||||
Min: 5,
|
||||
Max: 10_080,
|
||||
Start: 280,
|
||||
Target: 300,
|
||||
MeanReversion: 0.1,
|
||||
NoiseStdDev: 25,
|
||||
SpikeChance: 0.08,
|
||||
SpikeStdDev: 180,
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: _namespace + "hotfixes_per_release",
|
||||
help: "Average number of hotfixes per release.",
|
||||
label: "root_cause",
|
||||
buckets: []bucket{
|
||||
b.walk("works_on_my_machine", walkParams{
|
||||
Min: 0,
|
||||
Max: 12,
|
||||
Start: 0.9,
|
||||
Target: 1.0,
|
||||
MeanReversion: 0.1,
|
||||
NoiseStdDev: 0.2,
|
||||
SpikeChance: 0.05,
|
||||
SpikeStdDev: 1,
|
||||
}),
|
||||
b.walk("not_my_fault", walkParams{
|
||||
Min: 0,
|
||||
Max: 10,
|
||||
Start: 0.8,
|
||||
Target: 0.9,
|
||||
MeanReversion: 0.1,
|
||||
NoiseStdDev: 0.2,
|
||||
SpikeChance: 0.05,
|
||||
SpikeStdDev: 0.8,
|
||||
}),
|
||||
b.walk("timezone_issue", walkParams{
|
||||
Min: 0,
|
||||
Max: 5,
|
||||
Start: 0.4,
|
||||
Target: 0.45,
|
||||
MeanReversion: 0.1,
|
||||
NoiseStdDev: 0.1,
|
||||
SpikeChance: 0.04,
|
||||
SpikeStdDev: 0.5,
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: _namespace + "works_on_my_machine_rate",
|
||||
help: `Self-reported "works on my machine" incidence rate (0–100).`,
|
||||
label: "platform",
|
||||
buckets: []bucket{
|
||||
b.walk("linux", walkParams{
|
||||
Min: 10,
|
||||
Max: 60,
|
||||
Start: 28,
|
||||
Target: 30,
|
||||
MeanReversion: 0.06,
|
||||
NoiseStdDev: 2,
|
||||
SpikeChance: 0.05,
|
||||
SpikeStdDev: 6,
|
||||
}),
|
||||
b.walk("mac", walkParams{
|
||||
Min: 25,
|
||||
Max: 75,
|
||||
Start: 48,
|
||||
Target: 50,
|
||||
MeanReversion: 0.06,
|
||||
NoiseStdDev: 2.5,
|
||||
SpikeChance: 0.06,
|
||||
SpikeStdDev: 8,
|
||||
}),
|
||||
b.walk("my_special_setup", walkParams{
|
||||
Min: 60,
|
||||
Max: 100,
|
||||
Start: 82,
|
||||
Target: 85,
|
||||
MeanReversion: 0.05,
|
||||
NoiseStdDev: 3,
|
||||
SpikeChance: 0.07,
|
||||
SpikeStdDev: 5,
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: _namespace + "stale_branches_total",
|
||||
help: "Number of branches with no activity for more than 90 days.",
|
||||
label: "owner_status",
|
||||
buckets: []bucket{
|
||||
b.walk("still_employed", walkParams{
|
||||
Min: 0,
|
||||
Max: 800_000,
|
||||
Start: 95_000,
|
||||
Target: 120_000,
|
||||
MeanReversion: 0.03,
|
||||
NoiseStdDev: 6_000,
|
||||
DriftPerStep: 200,
|
||||
SpikeChance: 0.08,
|
||||
SpikeStdDev: 30_000,
|
||||
}),
|
||||
b.walk("left_the_company", walkParams{
|
||||
Min: 0,
|
||||
Max: 700_000,
|
||||
Start: 85_000,
|
||||
Target: 100_000,
|
||||
MeanReversion: 0.015,
|
||||
NoiseStdDev: 5_000,
|
||||
DriftPerStep: 300,
|
||||
SpikeChance: 0.07,
|
||||
SpikeStdDev: 25_000,
|
||||
}),
|
||||
b.walk("nobody_knows", walkParams{
|
||||
Min: 0,
|
||||
Max: 500_000,
|
||||
Start: 50_000,
|
||||
Target: 80_000,
|
||||
MeanReversion: 0.008,
|
||||
NoiseStdDev: 4_000,
|
||||
DriftPerStep: 100,
|
||||
SpikeChance: 0.07,
|
||||
SpikeStdDev: 20_000,
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: _namespace + "feature_flag_graveyard_size",
|
||||
help: "Number of feature flags that have outlived their purpose.",
|
||||
label: "removal_likelihood",
|
||||
buckets: []bucket{
|
||||
b.walk("maybe_someday", walkParams{
|
||||
Min: 20_000,
|
||||
Max: 2_000_000,
|
||||
Start: 200_000,
|
||||
Target: 240_000,
|
||||
MeanReversion: 0.02,
|
||||
NoiseStdDev: 15_000,
|
||||
DriftPerStep: 400,
|
||||
SpikeChance: 0.08,
|
||||
SpikeStdDev: 60_000,
|
||||
}),
|
||||
b.walk("never", walkParams{
|
||||
Min: 30_000,
|
||||
Max: 3_000_000,
|
||||
Start: 250_000,
|
||||
Target: 280_000,
|
||||
MeanReversion: 0.02,
|
||||
NoiseStdDev: 18_000,
|
||||
DriftPerStep: 500,
|
||||
SpikeChance: 0.09,
|
||||
SpikeStdDev: 70_000,
|
||||
}),
|
||||
b.walk("what_does_this_do", walkParams{
|
||||
Min: 5_000,
|
||||
Max: 1_000_000,
|
||||
Start: 70_000,
|
||||
Target: 80_000,
|
||||
MeanReversion: 0.02,
|
||||
NoiseStdDev: 8_000,
|
||||
DriftPerStep: 200,
|
||||
SpikeChance: 0.08,
|
||||
SpikeStdDev: 30_000,
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
330
server/core/internal/datasource/demo/registry_test.go
Normal file
330
server/core/internal/datasource/demo/registry_test.go
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
package demo
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_builder_next(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &builder{}
|
||||
|
||||
// every series gets a seed of its own, so no two draw the same
|
||||
// numbers.
|
||||
assert.Equal(t, int64(1), b.next())
|
||||
assert.Equal(t, int64(2), b.next())
|
||||
assert.Equal(t, int64(3), b.next())
|
||||
}
|
||||
|
||||
func Test_builder_count(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &builder{}
|
||||
|
||||
one := b.count("first", 1_000, 0.1)
|
||||
two := b.count("second", 1_000, 0.1)
|
||||
|
||||
assert.Equal(t, "first", one.label)
|
||||
assert.Equal(t, "second", two.label)
|
||||
|
||||
require.NotNil(t, one.value)
|
||||
|
||||
// the reading is the count the tick names, and two buckets declared
|
||||
// alike still read differently because their seeds differ.
|
||||
assert.Equal(t, countAt(1, 42, 1_000, 0.1), one.value(42))
|
||||
assert.NotEqual(t, one.value(42), two.value(42))
|
||||
}
|
||||
|
||||
func Test_builder_walk(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &builder{}
|
||||
|
||||
one := b.walk("first", _testWalk)
|
||||
two := b.walk("second", _testWalk)
|
||||
|
||||
assert.Equal(t, "first", one.label)
|
||||
require.NotNil(t, one.value)
|
||||
|
||||
// the bucket reads its walk, which starts where the params say.
|
||||
assert.Equal(t, _testWalk.Start, one.value(0))
|
||||
|
||||
// same params, different seed, so the two diverge once they move.
|
||||
assert.NotEqual(t, one.value(100), two.value(100))
|
||||
}
|
||||
|
||||
func Test_builder_deploys(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
bb := (&builder{}).deploys()
|
||||
require.Len(t, bb, 2)
|
||||
|
||||
assert.Equal(t, "attempted", bb[0].label)
|
||||
assert.Equal(t, "successful", bb[1].label)
|
||||
|
||||
// the successful bucket is a share of the attempted one, so a chart
|
||||
// of both never shows more deploys surviving than were tried.
|
||||
for tick := range int64(500) {
|
||||
attempted := bb[0].value(tick)
|
||||
successful := bb[1].value(tick)
|
||||
|
||||
assert.GreaterOrEqual(t, successful, 0.0)
|
||||
assert.LessOrEqual(t, successful, attempted, "tick %d", tick)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_newRegistry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ff := newRegistry()
|
||||
require.Len(t, ff, 15)
|
||||
|
||||
var (
|
||||
names = map[string]struct{}{}
|
||||
seen = map[string]float64{}
|
||||
buckets int
|
||||
)
|
||||
|
||||
for _, f := range ff {
|
||||
// every family is namespaced, described, and populated.
|
||||
assert.True(t, strings.HasPrefix(f.name, _namespace), f.name)
|
||||
assert.NotEmpty(t, f.help, f.name)
|
||||
require.NotEmpty(t, f.buckets, f.name)
|
||||
|
||||
_, dup := names[f.name]
|
||||
assert.False(t, dup, "duplicate family %q", f.name)
|
||||
names[f.name] = struct{}{}
|
||||
|
||||
bucketLabels := map[string]struct{}{}
|
||||
|
||||
for _, b := range f.buckets {
|
||||
buckets++
|
||||
|
||||
require.NotNil(t, b.value, f.name)
|
||||
|
||||
// a family either breaks down by a label or publishes one
|
||||
// unlabelled series; a bucket never disagrees with its family.
|
||||
if f.label == "" {
|
||||
assert.Empty(t, b.label, f.name)
|
||||
assert.Len(t, f.buckets, 1, f.name)
|
||||
} else {
|
||||
assert.NotEmpty(t, b.label, f.name)
|
||||
}
|
||||
|
||||
_, dup := bucketLabels[b.label]
|
||||
assert.False(t, dup, "duplicate bucket %q in %q", b.label, f.name)
|
||||
bucketLabels[b.label] = struct{}{}
|
||||
|
||||
// every series reads, and no two read alike — a seed shared by
|
||||
// accident would show up here as two identical lines.
|
||||
v := b.value(1_000)
|
||||
key := f.name + "/" + b.label
|
||||
|
||||
for other, ov := range seen {
|
||||
assert.NotEqual(t, ov, v, "%s and %s read alike", other, key)
|
||||
}
|
||||
|
||||
seen[key] = v
|
||||
}
|
||||
}
|
||||
|
||||
assert.Len(t, seen, buckets)
|
||||
}
|
||||
|
||||
func Test_registry_isCachedPerClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// the client holds one registry for its lifetime: the walks behind it
|
||||
// cache the history they replay, and rebuilding it per query would
|
||||
// walk every tick since the epoch again.
|
||||
c := NewClient()
|
||||
|
||||
first := c.registry
|
||||
require.NotEmpty(t, first)
|
||||
|
||||
assert.Equal(t, first[0].buckets[0].value(7), c.registry[0].buckets[0].value(7))
|
||||
|
||||
// a separate client is a separate cache, reading the same history.
|
||||
other := NewClient()
|
||||
require.Len(t, other.registry, len(first))
|
||||
|
||||
for i := range first {
|
||||
assert.Equal(t, first[i].name, other.registry[i].name)
|
||||
assert.Equal(t, first[i].buckets[0].value(7), other.registry[i].buckets[0].value(7))
|
||||
}
|
||||
}
|
||||
|
||||
// nameMatcher builds a matcher selecting one metric by name, failing
|
||||
// the test if it cannot.
|
||||
func nameMatcher(t *testing.T, name string) *labels.Matcher {
|
||||
t.Helper()
|
||||
|
||||
m, err := labels.NewMatcher(labels.MatchEqual, model.MetricNameLabel, name)
|
||||
require.NoError(t, err)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func Test_seriesLabels(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// a labelled family carries its breakdown alongside the metric name.
|
||||
lbls := seriesLabels(
|
||||
family{name: "metric", label: "colour"},
|
||||
bucket{label: "red"},
|
||||
)
|
||||
|
||||
assert.Equal(t, "metric", lbls.Get(model.MetricNameLabel))
|
||||
assert.Equal(t, "red", lbls.Get("colour"))
|
||||
assert.Equal(t, 2, lbls.Len())
|
||||
|
||||
// an unlabelled one carries nothing but the name — not an empty
|
||||
// label, which would show up in autocomplete as a real dimension.
|
||||
lbls = seriesLabels(family{name: "metric"}, bucket{})
|
||||
|
||||
assert.Equal(t, "metric", lbls.Get(model.MetricNameLabel))
|
||||
assert.Equal(t, 1, lbls.Len())
|
||||
}
|
||||
|
||||
func Test_matches(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
lbls := labels.FromStrings(model.MetricNameLabel, "metric", "colour", "red")
|
||||
|
||||
cc := map[string]struct {
|
||||
Matchers []*labels.Matcher
|
||||
Result bool
|
||||
}{
|
||||
"No matchers at all": {Result: true},
|
||||
"Matching name": {Matchers: []*labels.Matcher{{Type: labels.MatchEqual, Name: model.MetricNameLabel, Value: "metric"}}, Result: true},
|
||||
"Other name": {Matchers: []*labels.Matcher{{Type: labels.MatchEqual, Name: model.MetricNameLabel, Value: "nope"}}},
|
||||
"Matching label": {Matchers: []*labels.Matcher{{Type: labels.MatchEqual, Name: "colour", Value: "red"}}, Result: true},
|
||||
"Other label value": {Matchers: []*labels.Matcher{{Type: labels.MatchEqual, Name: "colour", Value: "blue"}}},
|
||||
"Absent label matched empty": {
|
||||
Matchers: []*labels.Matcher{{Type: labels.MatchEqual, Name: "size", Value: ""}},
|
||||
Result: true,
|
||||
},
|
||||
"One of several fails": {
|
||||
Matchers: []*labels.Matcher{
|
||||
{Type: labels.MatchEqual, Name: model.MetricNameLabel, Value: "metric"},
|
||||
{Type: labels.MatchEqual, Name: "colour", Value: "blue"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Equal(t, c.Result, matches(lbls, c.Matchers))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_registry_forEach(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// no matchers reaches every series the registry declares.
|
||||
var all int
|
||||
|
||||
newRegistry().forEach(nil, func(labels.Labels, func(int64) float64) { all++ })
|
||||
|
||||
var buckets int
|
||||
|
||||
for _, f := range newRegistry() {
|
||||
buckets += len(f.buckets)
|
||||
}
|
||||
|
||||
assert.Equal(t, buckets, all)
|
||||
|
||||
// a matcher narrows to the family it names, and every series handed
|
||||
// over carries a reading.
|
||||
var named int
|
||||
|
||||
newRegistry().forEach(
|
||||
[]*labels.Matcher{nameMatcher(t, _namespace+"deploy_confidence_index")},
|
||||
func(lbls labels.Labels, value func(int64) float64) {
|
||||
named++
|
||||
|
||||
assert.Equal(t, _namespace+"deploy_confidence_index", lbls.Get(model.MetricNameLabel))
|
||||
require.NotNil(t, value)
|
||||
assert.NotZero(t, value(500))
|
||||
},
|
||||
)
|
||||
|
||||
assert.Equal(t, 3, named)
|
||||
|
||||
// a matcher naming nothing reaches nothing.
|
||||
newRegistry().forEach(
|
||||
[]*labels.Matcher{nameMatcher(t, "not_a_metric")},
|
||||
func(labels.Labels, func(int64) float64) { t.Fatal("matched a series that does not exist") },
|
||||
)
|
||||
}
|
||||
|
||||
func Test_sortedKeys(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Equal(t, []string{"a", "b", "c"}, sortedKeys(map[string]struct{}{
|
||||
"c": {},
|
||||
"a": {},
|
||||
"b": {},
|
||||
}))
|
||||
|
||||
// an empty set answers an empty slice rather than nil, so a JSON
|
||||
// response carries [] instead of null.
|
||||
assert.Equal(t, []string{}, sortedKeys(map[string]struct{}{}))
|
||||
}
|
||||
|
||||
func Test_registry_labelNames(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
names := newRegistry().labelNames([][]*labels.Matcher{nil})
|
||||
|
||||
assert.Contains(t, names, model.MetricNameLabel)
|
||||
assert.Contains(t, names, "vibe")
|
||||
assert.Contains(t, names, "severity")
|
||||
assert.IsIncreasing(t, names)
|
||||
|
||||
// narrowing to one family leaves only that family's dimension.
|
||||
names = newRegistry().labelNames([][]*labels.Matcher{
|
||||
{nameMatcher(t, _namespace+"deploy_confidence_index")},
|
||||
})
|
||||
|
||||
assert.Equal(t, []string{model.MetricNameLabel, "vibe"}, names)
|
||||
|
||||
// several selectors are unioned, the way the endpoint answers when
|
||||
// handed more than one.
|
||||
names = newRegistry().labelNames([][]*labels.Matcher{
|
||||
{nameMatcher(t, _namespace+"deploy_confidence_index")},
|
||||
{nameMatcher(t, _namespace+"secrets_detected_commits_count")},
|
||||
})
|
||||
|
||||
assert.Equal(t, []string{model.MetricNameLabel, "severity", "vibe"}, names)
|
||||
}
|
||||
|
||||
func Test_registry_labelValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// the metric names are a label value like any other, which is what
|
||||
// the editor's metric autocomplete reads.
|
||||
names := newRegistry().labelValues(model.MetricNameLabel, [][]*labels.Matcher{nil})
|
||||
|
||||
assert.Len(t, names, 15)
|
||||
assert.Contains(t, names, _namespace+"pizza_fridays_next_30_days")
|
||||
assert.IsIncreasing(t, names)
|
||||
|
||||
assert.Equal(
|
||||
t,
|
||||
[]string{"feeling_lucky", "nervous", "oncall_already_paged"},
|
||||
newRegistry().labelValues("vibe", [][]*labels.Matcher{nil}),
|
||||
)
|
||||
|
||||
// a label no series carries answers empty rather than a blank value.
|
||||
assert.Empty(t, newRegistry().labelValues("nonexistent", [][]*labels.Matcher{nil}))
|
||||
}
|
||||
215
server/core/internal/datasource/demo/series.go
Normal file
215
server/core/internal/datasource/demo/series.go
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
package demo
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/oxynote/oxynote/server/core/pkg/timeutil"
|
||||
)
|
||||
|
||||
// _tick is the interval between two demo samples.
|
||||
const _tick = time.Minute
|
||||
|
||||
// _tickMillis is that same interval in the milliseconds the storage layer
|
||||
// counts in.
|
||||
const _tickMillis = int64(_tick / time.Millisecond)
|
||||
|
||||
// _segment is how many ticks one replay segment spans. A sample replays
|
||||
// at most this many steps from the checkpoint before it, which is what
|
||||
// keeps a query over a year from walking every tick since the epoch.
|
||||
const _segment = 1440
|
||||
|
||||
// _seedStride separates the random streams of two series whose segment
|
||||
// numbers differ by one, so no two streams overlap.
|
||||
const _seedStride = 1_000_003
|
||||
|
||||
// _epoch is the instant the demo timeline starts. It is fixed so that a
|
||||
// tick always denotes the same moment, on every install and every run.
|
||||
var _epoch = time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// tickAt reports which tick covers the given instant.
|
||||
func tickAt(t time.Time) int64 {
|
||||
return int64(t.Sub(_epoch) / _tick)
|
||||
}
|
||||
|
||||
// tickAtMillis reports which tick covers the given millisecond instant.
|
||||
func tickAtMillis(ms int64) int64 {
|
||||
return (ms - _epoch.UnixMilli()) / _tickMillis
|
||||
}
|
||||
|
||||
// timeAt reports the instant the given tick starts at.
|
||||
func timeAt(tick int64) time.Time {
|
||||
return _epoch.Add(time.Duration(tick) * _tick)
|
||||
}
|
||||
|
||||
// latestTick reports the last tick the timeline has reached. Nothing past
|
||||
// it is ever sampled: the demo has no more happened in the future than a
|
||||
// real deployment has.
|
||||
func latestTick() int64 {
|
||||
return tickAt(timeutil.Now())
|
||||
}
|
||||
|
||||
// newRand returns the random stream the given seed and stream number
|
||||
// name. Every segment of every series draws from its own stream, so a
|
||||
// value never depends on which ticks were read before it.
|
||||
func newRand(seed, stream int64) *rand.Rand {
|
||||
//nolint:gosec // demo data wants a cheap reproducible source, not a cryptographic one
|
||||
return rand.New(rand.NewSource(seed*_seedStride + stream))
|
||||
}
|
||||
|
||||
// normal returns a sample from N(mean, stddev) via the Box-Muller
|
||||
// transform.
|
||||
func normal(r *rand.Rand, mean, stddev float64) float64 {
|
||||
u1 := r.Float64()
|
||||
u2 := r.Float64()
|
||||
z := math.Sqrt(-2.0*math.Log(u1)) * math.Cos(2.0*math.Pi*u2)
|
||||
|
||||
return mean + stddev*z
|
||||
}
|
||||
|
||||
// clamp returns x clamped to [lo, hi].
|
||||
func clamp(x, lo, hi float64) float64 {
|
||||
if x < lo {
|
||||
return lo
|
||||
}
|
||||
|
||||
if x > hi {
|
||||
return hi
|
||||
}
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
// countAt returns the count the given bucket reports at the given tick.
|
||||
// A count is resampled from scratch every tick, so it needs no history.
|
||||
func countAt(seed, tick int64, mean, relStdDev float64) float64 {
|
||||
v := math.Round(normal(newRand(seed, tick), mean, mean*relStdDev))
|
||||
if v < 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// walkParams configures a gauge that evolves each tick via drift and mean
|
||||
// reversion.
|
||||
type walkParams struct {
|
||||
// Min is the lower bound of the gauge value.
|
||||
Min float64
|
||||
|
||||
// Max is the upper bound of the gauge value.
|
||||
Max float64
|
||||
|
||||
// Start is the value the walk begins at, at the epoch.
|
||||
Start float64
|
||||
|
||||
// DriftPerStep is a constant added each tick, modelling a slow
|
||||
// linear trend. Mean reversion absorbs it, so the walk settles
|
||||
// around Target+DriftPerStep/MeanReversion rather than running away.
|
||||
DriftPerStep float64
|
||||
|
||||
// NoiseStdDev is the standard deviation of the Gaussian noise added
|
||||
// each tick.
|
||||
NoiseStdDev float64
|
||||
|
||||
// SpikeChance is the probability (0–1) of an additional spike
|
||||
// occurring on any given tick.
|
||||
SpikeChance float64
|
||||
|
||||
// SpikeStdDev is the standard deviation of the spike magnitude when
|
||||
// one occurs.
|
||||
SpikeStdDev float64
|
||||
|
||||
// MeanReversion is the fraction (0–1) of the distance to Target
|
||||
// pulled back each tick.
|
||||
MeanReversion float64
|
||||
|
||||
// Target is the attractor value for mean reversion.
|
||||
Target float64
|
||||
}
|
||||
|
||||
// walk replays a mean-reverting random walk. The value at a tick depends
|
||||
// only on the walk's seed and the tick's distance from the epoch, never on
|
||||
// which ticks were read before it — so every query sees the same history,
|
||||
// and a line already drawn never rewrites itself.
|
||||
type walk struct {
|
||||
// params govern the walk's drift, noise and mean reversion.
|
||||
params walkParams
|
||||
|
||||
// seed distinguishes this walk's random streams from every other
|
||||
// series'.
|
||||
seed int64
|
||||
|
||||
// mu guards checkpoints.
|
||||
mu sync.Mutex
|
||||
|
||||
// checkpoints holds the walk's value at the start of each segment,
|
||||
// grown on demand. Index i is the value at tick i*_segment.
|
||||
checkpoints []float64
|
||||
}
|
||||
|
||||
// newWalk creates a fresh instance of walk.
|
||||
func newWalk(seed int64, params walkParams) *walk {
|
||||
return &walk{
|
||||
params: params,
|
||||
seed: seed,
|
||||
}
|
||||
}
|
||||
|
||||
// at returns the walk's value at the given tick.
|
||||
func (w *walk) at(tick int64) float64 {
|
||||
if tick < 0 {
|
||||
tick = 0
|
||||
}
|
||||
|
||||
segment := tick / _segment
|
||||
|
||||
return replay(
|
||||
w.checkpoint(segment),
|
||||
w.params,
|
||||
newRand(w.seed, segment),
|
||||
int(tick%_segment),
|
||||
)
|
||||
}
|
||||
|
||||
// checkpoint returns the walk's value at the start of the given segment,
|
||||
// computing and caching every segment before it that is not cached yet.
|
||||
func (w *walk) checkpoint(segment int64) float64 {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if len(w.checkpoints) == 0 {
|
||||
w.checkpoints = []float64{clamp(w.params.Start, w.params.Min, w.params.Max)}
|
||||
}
|
||||
|
||||
for int64(len(w.checkpoints)) <= segment {
|
||||
last := int64(len(w.checkpoints)) - 1
|
||||
|
||||
w.checkpoints = append(w.checkpoints, replay(
|
||||
w.checkpoints[last],
|
||||
w.params,
|
||||
newRand(w.seed, last),
|
||||
_segment,
|
||||
))
|
||||
}
|
||||
|
||||
return w.checkpoints[segment]
|
||||
}
|
||||
|
||||
// replay advances v by the given number of the walk's ticks.
|
||||
func replay(v float64, p walkParams, r *rand.Rand, steps int) float64 {
|
||||
for range steps {
|
||||
v += p.MeanReversion * (p.Target - v)
|
||||
v += p.DriftPerStep + normal(r, 0, p.NoiseStdDev)
|
||||
|
||||
if r.Float64() < p.SpikeChance {
|
||||
v += normal(r, 0, p.SpikeStdDev)
|
||||
}
|
||||
|
||||
v = clamp(v, p.Min, p.Max)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
283
server/core/internal/datasource/demo/series_test.go
Normal file
283
server/core/internal/datasource/demo/series_test.go
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
package demo
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/oxynote/oxynote/server/core/pkg/timeutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// _testWalk is a walk whose bounds are wide enough that clamping never
|
||||
// hides a mistake in the replay itself.
|
||||
var _testWalk = walkParams{
|
||||
Min: 0,
|
||||
Max: 1_000,
|
||||
Start: 100,
|
||||
Target: 120,
|
||||
MeanReversion: 0.05,
|
||||
NoiseStdDev: 5,
|
||||
DriftPerStep: 0.1,
|
||||
SpikeChance: 0.05,
|
||||
SpikeStdDev: 20,
|
||||
}
|
||||
|
||||
func Test_tickAt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
Time time.Time
|
||||
Result int64
|
||||
}{
|
||||
"The epoch itself": {Time: _epoch, Result: 0},
|
||||
"Part way into a tick": {Time: _epoch.Add(30 * time.Second), Result: 0},
|
||||
"The next tick": {Time: _epoch.Add(time.Minute), Result: 1},
|
||||
"An hour in": {Time: _epoch.Add(time.Hour), Result: 60},
|
||||
"A day in": {Time: _epoch.Add(24 * time.Hour), Result: _segment},
|
||||
"Before the epoch": {
|
||||
Time: _epoch.Add(-time.Hour),
|
||||
Result: -60,
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Equal(t, c.Result, tickAt(c.Time))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_tickAtMillis(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
Millis int64
|
||||
Result int64
|
||||
}{
|
||||
"The epoch itself": {Millis: _epoch.UnixMilli(), Result: 0},
|
||||
"Part way into a tick": {Millis: _epoch.UnixMilli() + 30_000, Result: 0},
|
||||
"The next tick": {Millis: _epoch.UnixMilli() + 60_000, Result: 1},
|
||||
"A day in": {Millis: _epoch.Add(24 * time.Hour).UnixMilli(), Result: _segment},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Equal(t, c.Result, tickAtMillis(c.Millis))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_timeAt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Equal(t, _epoch, timeAt(0))
|
||||
assert.Equal(t, _epoch.Add(time.Minute), timeAt(1))
|
||||
|
||||
// the two directions agree, which is what lets a sample timestamp be
|
||||
// turned back into the tick that produced its value.
|
||||
for _, tick := range []int64{0, 1, 59, _segment, 1_000_000} {
|
||||
assert.Equal(t, tick, tickAt(timeAt(tick)))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_latestTick(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := latestTick()
|
||||
|
||||
// nothing past the present is ever sampled, and the timeline has
|
||||
// reached the present.
|
||||
assert.Equal(t, tickAt(timeutil.Now()), got)
|
||||
assert.Positive(t, got)
|
||||
assert.False(t, timeAt(got).After(timeutil.Now()))
|
||||
}
|
||||
|
||||
func Test_newRand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// the same seed and stream replay the same numbers, which is the
|
||||
// whole basis of a frozen history.
|
||||
assert.Equal(t, newRand(1, 2).Float64(), newRand(1, 2).Float64())
|
||||
|
||||
// a different seed or a different stream is a different history.
|
||||
assert.NotEqual(t, newRand(1, 2).Float64(), newRand(2, 2).Float64())
|
||||
assert.NotEqual(t, newRand(1, 2).Float64(), newRand(1, 3).Float64())
|
||||
|
||||
// neighbouring series do not overlap: seed 1's stream 0 is not seed
|
||||
// 0's stream _seedStride read one segment later.
|
||||
assert.NotEqual(t, newRand(1, 0).Float64(), newRand(0, 1).Float64())
|
||||
}
|
||||
|
||||
func Test_normal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// with no spread every draw is the mean.
|
||||
assert.InDelta(t, 7.0, normal(newRand(1, 1), 7, 0), 1e-9)
|
||||
|
||||
// the same stream draws the same number twice.
|
||||
assert.Equal(t, normal(newRand(1, 1), 0, 1), normal(newRand(1, 1), 0, 1))
|
||||
|
||||
// over many draws the sample mean lands near the stated mean.
|
||||
r := newRand(3, 3)
|
||||
sum := 0.0
|
||||
|
||||
for range 10_000 {
|
||||
sum += normal(r, 5, 2)
|
||||
}
|
||||
|
||||
assert.InDelta(t, 5.0, sum/10_000, 0.1)
|
||||
}
|
||||
|
||||
func Test_clamp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
X float64
|
||||
Lo float64
|
||||
Hi float64
|
||||
Result float64
|
||||
}{
|
||||
"Within the bounds": {X: 5, Lo: 0, Hi: 10, Result: 5},
|
||||
"Below the floor": {X: -1, Lo: 0, Hi: 10, Result: 0},
|
||||
"Above the ceiling": {X: 11, Lo: 0, Hi: 10, Result: 10},
|
||||
"On the floor": {X: 0, Lo: 0, Hi: 10, Result: 0},
|
||||
"On the ceiling": {X: 10, Lo: 0, Hi: 10, Result: 10},
|
||||
"Infinite": {X: math.Inf(1), Lo: 0, Hi: 10, Result: 10},
|
||||
"Negative infinite": {X: math.Inf(-1), Lo: 0, Hi: 10, Result: 0},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Equal(t, c.Result, clamp(c.X, c.Lo, c.Hi))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_countAt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// a count depends on its tick alone, so reading it again answers the
|
||||
// same and reading a neighbour does not.
|
||||
assert.Equal(t, countAt(1, 10, 1_000, 0.15), countAt(1, 10, 1_000, 0.15))
|
||||
assert.NotEqual(t, countAt(1, 10, 1_000, 0.15), countAt(1, 11, 1_000, 0.15))
|
||||
assert.NotEqual(t, countAt(1, 10, 1_000, 0.15), countAt(2, 10, 1_000, 0.15))
|
||||
|
||||
// counts are whole and never negative, however wide the spread.
|
||||
for tick := range int64(500) {
|
||||
v := countAt(9, tick, 4, 5)
|
||||
|
||||
assert.GreaterOrEqual(t, v, 0.0)
|
||||
assert.Equal(t, math.Trunc(v), v)
|
||||
}
|
||||
|
||||
// across many ticks the average lands near the stated mean.
|
||||
sum := 0.0
|
||||
|
||||
for tick := range int64(5_000) {
|
||||
sum += countAt(4, tick, 2_000, 0.15)
|
||||
}
|
||||
|
||||
assert.InDelta(t, 2_000.0, sum/5_000, 50)
|
||||
}
|
||||
|
||||
func Test_replay(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// no steps is no movement.
|
||||
assert.Equal(t, 100.0, replay(100, _testWalk, newRand(1, 1), 0))
|
||||
|
||||
// the same start and stream replay to the same value.
|
||||
assert.Equal(
|
||||
t,
|
||||
replay(100, _testWalk, newRand(1, 1), 50),
|
||||
replay(100, _testWalk, newRand(1, 1), 50),
|
||||
)
|
||||
|
||||
// every step clamps, so a walk started far outside its bounds is
|
||||
// inside them again after one.
|
||||
p := _testWalk
|
||||
p.Min = 10
|
||||
p.Max = 20
|
||||
|
||||
assert.LessOrEqual(t, replay(1_000, p, newRand(1, 1), 1), 20.0)
|
||||
assert.GreaterOrEqual(t, replay(-1_000, p, newRand(1, 1), 1), 10.0)
|
||||
}
|
||||
|
||||
func Test_newWalk(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := newWalk(42, _testWalk)
|
||||
require.NotNil(t, w)
|
||||
|
||||
assert.Equal(t, int64(42), w.seed)
|
||||
assert.Equal(t, _testWalk, w.params)
|
||||
|
||||
// nothing is computed until something is asked for.
|
||||
assert.Empty(t, w.checkpoints)
|
||||
}
|
||||
|
||||
func Test_walk_at(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// two walks built from the same seed answer identically, which is
|
||||
// what makes the history the same on every install rather than only
|
||||
// within one process.
|
||||
a, b := newWalk(5, _testWalk), newWalk(5, _testWalk)
|
||||
|
||||
for _, tick := range []int64{0, 1, 100, _segment - 1, _segment, _segment + 1, 3*_segment + 7} {
|
||||
assert.Equal(t, a.at(tick), b.at(tick), "tick %d", tick)
|
||||
}
|
||||
|
||||
// reading out of order answers the same as reading in order: a value
|
||||
// depends on its tick, not on what was read before it.
|
||||
c := newWalk(5, _testWalk)
|
||||
assert.Equal(t, a.at(2*_segment+3), c.at(2*_segment+3))
|
||||
|
||||
// the walk starts where it was told to and stays inside its bounds.
|
||||
assert.Equal(t, _testWalk.Start, a.at(0))
|
||||
|
||||
for _, tick := range []int64{1, 500, _segment, 5_000} {
|
||||
assert.GreaterOrEqual(t, a.at(tick), _testWalk.Min)
|
||||
assert.LessOrEqual(t, a.at(tick), _testWalk.Max)
|
||||
}
|
||||
|
||||
// a tick before the epoch reads as the epoch rather than running the
|
||||
// walk backwards.
|
||||
assert.Equal(t, a.at(0), a.at(-10))
|
||||
|
||||
// a segment boundary is continuous: the first tick of a segment is
|
||||
// the value the previous segment ended on.
|
||||
assert.Equal(t, a.checkpoint(1), a.at(_segment))
|
||||
}
|
||||
|
||||
func Test_walk_checkpoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := newWalk(11, _testWalk)
|
||||
|
||||
// the first checkpoint is the start value, clamped.
|
||||
assert.Equal(t, _testWalk.Start, w.checkpoint(0))
|
||||
assert.Len(t, w.checkpoints, 1)
|
||||
|
||||
// asking for a later one fills in every segment before it, once.
|
||||
assert.NotZero(t, w.checkpoint(3))
|
||||
assert.Len(t, w.checkpoints, 4)
|
||||
|
||||
cached := w.checkpoints[3]
|
||||
assert.Equal(t, cached, w.checkpoint(3))
|
||||
assert.Len(t, w.checkpoints, 4)
|
||||
|
||||
// a start outside the bounds is pulled inside before anything runs.
|
||||
p := _testWalk
|
||||
p.Start = 5_000
|
||||
|
||||
assert.Equal(t, p.Max, newWalk(12, p).checkpoint(0))
|
||||
}
|
||||
|
|
@ -27,6 +27,20 @@ const (
|
|||
_queryPayloadLimit = 5 * 1024 * 1024 // 5 MB
|
||||
)
|
||||
|
||||
// _pgAllowedConnKeys lists the connection-string keys a data-source URL may
|
||||
// carry. The URL is organisation-supplied, and keys outside this list can
|
||||
// reach the local filesystem (sslkey, passfile, servicefile, ...) or override
|
||||
// session behavior (options), so parsing fails closed on anything else.
|
||||
var _pgAllowedConnKeys = []string{
|
||||
"host",
|
||||
"port",
|
||||
"dbname",
|
||||
"user",
|
||||
"password",
|
||||
"sslmode",
|
||||
"connect_timeout",
|
||||
}
|
||||
|
||||
// PostgreSQL represents a PostgreSQL data source processor.
|
||||
type PostgreSQL struct {
|
||||
inp Input
|
||||
|
|
@ -261,7 +275,9 @@ func (p *PostgreSQL) connect(ctx context.Context) (*pgx.Conn, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err := pgx.ParseConfig(connStr)
|
||||
cfg, err := pgx.ParseConfigWithOptions(connStr, pgx.ParseConfigOptions{
|
||||
ConnStringAllowedKeys: _pgAllowedConnKeys,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing postgresql connection string: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -693,9 +693,10 @@ func Test_PostgreSQL_connect(t *testing.T) {
|
|||
_, err = p.connect(context.Background())
|
||||
assert.Error(t, err)
|
||||
|
||||
// the session refuses writes, and the simple protocol cannot be
|
||||
// the session refuses writes, and connection-string keys outside the
|
||||
// allowlist are rejected at parse, so the simple protocol cannot be
|
||||
// re-enabled through the data source URL to smuggle a second statement
|
||||
// past the gate.
|
||||
// past the gate, and the URL cannot reach the local filesystem.
|
||||
cc := map[string]struct {
|
||||
URL string
|
||||
Query string
|
||||
|
|
@ -708,6 +709,18 @@ func Test_PostgreSQL_connect(t *testing.T) {
|
|||
URL: pgTestURL(_pgUser, _pgPass) + "&default_query_exec_mode=simple_protocol",
|
||||
Query: "SET transaction_read_only = off; DELETE FROM metrics",
|
||||
},
|
||||
"Disallowed sslkey key": {
|
||||
URL: pgTestURL(_pgUser, _pgPass) + "&sslkey=/tmp/key",
|
||||
Query: "SELECT time FROM metrics",
|
||||
},
|
||||
"Disallowed servicefile key": {
|
||||
URL: pgTestURL(_pgUser, _pgPass) + "&servicefile=/tmp/service",
|
||||
Query: "SELECT time FROM metrics",
|
||||
},
|
||||
"Disallowed options key": {
|
||||
URL: pgTestURL(_pgUser, _pgPass) + "&options=-csearch_path%3Dpublic",
|
||||
Query: "SELECT time FROM metrics",
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
|
|
@ -721,7 +734,12 @@ func Test_PostgreSQL_connect(t *testing.T) {
|
|||
_, err := p.Query(context.Background(), c.Query, _testTimeRange)
|
||||
assert.Error(t, err)
|
||||
|
||||
// the rows are still there.
|
||||
// the rows are still there, checked over a clean URL since the
|
||||
// case's own URL may be the thing being rejected.
|
||||
p = NewPostgreSQL(&InputMock{
|
||||
URLFunc: func() string { return pgTestURL(_pgUser, _pgPass) },
|
||||
})
|
||||
|
||||
res, err := p.Query(context.Background(), "SELECT time FROM metrics ORDER BY time", _testTimeRange)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, res.Rows, 2)
|
||||
|
|
|
|||
|
|
@ -141,9 +141,14 @@ func (p *Prometheus) LabelNames(ctx context.Context, matchers []string, tr TimeR
|
|||
return nil, fmt.Errorf("error fetching label names: %w", err)
|
||||
}
|
||||
|
||||
result := make([]string, len(names))
|
||||
for i, n := range names {
|
||||
result[i] = string(n)
|
||||
}
|
||||
|
||||
return &PrometheusLabelNamesResult{
|
||||
Warnings: warns,
|
||||
Result: names,
|
||||
Result: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ import (
|
|||
"log/slog"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/oxynote/oxynote/server/core/internal/datasource/demo"
|
||||
"github.com/oxynote/oxynote/server/core/internal/datasource/processor"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/errutil"
|
||||
"github.com/rs/xid"
|
||||
|
|
@ -21,6 +23,7 @@ import (
|
|||
type Manager struct {
|
||||
log *slog.Logger
|
||||
store StatusStore
|
||||
demo *demo.Client
|
||||
}
|
||||
|
||||
// NewManager creates a fresh instance of Manager.
|
||||
|
|
@ -28,12 +31,13 @@ func NewManager(log *slog.Logger, store StatusStore) *Manager {
|
|||
return &Manager{
|
||||
log: log.With("component", "datasource"),
|
||||
store: store,
|
||||
demo: demo.NewClient(),
|
||||
}
|
||||
}
|
||||
|
||||
// Runner returns the runner that operates the given data source.
|
||||
func (m *Manager) Runner(ds DataSource) Runner {
|
||||
return &runner{ds: ds, log: m.log, store: m.store}
|
||||
return &runner{ds: ds, log: m.log, store: m.store, demo: m.demo}
|
||||
}
|
||||
|
||||
// runner operates one data source.
|
||||
|
|
@ -54,6 +58,10 @@ type runner struct {
|
|||
|
||||
// client is the processor this runner's type resolves to.
|
||||
client connectionTester
|
||||
|
||||
// demo answers the data source this process synthesizes, shared with
|
||||
// every other runner the manager hands out.
|
||||
demo *demo.Client
|
||||
}
|
||||
|
||||
// Type reports what the data source speaks.
|
||||
|
|
@ -172,7 +180,12 @@ func (r *runner) ensurePrepared() error {
|
|||
|
||||
switch r.ds.Type {
|
||||
case TypePrometheus:
|
||||
r.client = processor.NewPrometheus(newStateInput(r.ds.URL, r.ds.Credentials))
|
||||
client, err := r.prometheusClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.client = client
|
||||
case TypePostgreSQL:
|
||||
r.client = processor.NewPostgreSQL(newStateInput(r.ds.URL, r.ds.Credentials))
|
||||
case TypeMariaDB, TypeMySQL:
|
||||
|
|
@ -186,6 +199,22 @@ func (r *runner) ensurePrepared() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// prometheusClient resolves which Prometheus a data source speaks to. The
|
||||
// demo scheme names a source this process synthesizes rather than one it
|
||||
// dials, and only the one demo source exists — a URL that merely looks
|
||||
// like it is a mistake worth reporting, not a second demo.
|
||||
func (r *runner) prometheusClient() (connectionTester, error) {
|
||||
if r.ds.URL == demo.URL {
|
||||
return r.demo, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(r.ds.URL, demo.Scheme) {
|
||||
return nil, demo.ErrUnknownSource
|
||||
}
|
||||
|
||||
return processor.NewPrometheus(newStateInput(r.ds.URL, r.ds.Credentials)), nil
|
||||
}
|
||||
|
||||
// Runner operates one data source: it hands out the typed client for
|
||||
// the operation being performed, or the reason it cannot.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/oxynote/oxynote/server/core/internal/datasource/demo"
|
||||
"github.com/oxynote/oxynote/server/core/internal/datasource/processor"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/prometheus/client_golang/api"
|
||||
|
|
@ -394,20 +395,23 @@ func Test_runner_ensurePrepared(t *testing.T) {
|
|||
|
||||
cc := map[string]struct {
|
||||
Type Type
|
||||
URL string
|
||||
Err error
|
||||
}{
|
||||
"Prometheus": {Type: TypePrometheus},
|
||||
"PostgreSQL": {Type: TypePostgreSQL},
|
||||
"MySQL": {Type: TypeMySQL},
|
||||
"MariaDB": {Type: TypeMariaDB},
|
||||
"Unknown type": {Type: Type("bogus"), Err: assert.AnError},
|
||||
"Prometheus": {Type: TypePrometheus},
|
||||
"Demo Prometheus": {Type: TypePrometheus, URL: demo.URL},
|
||||
"Unknown demo source": {Type: TypePrometheus, URL: demo.Scheme + "nope", Err: demo.ErrUnknownSource},
|
||||
"PostgreSQL": {Type: TypePostgreSQL},
|
||||
"MySQL": {Type: TypeMySQL},
|
||||
"MariaDB": {Type: TypeMariaDB},
|
||||
"Unknown type": {Type: Type("bogus"), Err: assert.AnError},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := prepRunner(c.Type, "", nil)
|
||||
r := prepRunner(c.Type, c.URL, nil)
|
||||
|
||||
err := r.ensurePrepared()
|
||||
|
||||
|
|
@ -428,3 +432,46 @@ func Test_runner_ensurePrepared(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_runner_prometheusClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cc := map[string]struct {
|
||||
URL string
|
||||
Client connectionTester
|
||||
Err error
|
||||
}{
|
||||
"Demo data source": {
|
||||
URL: demo.URL,
|
||||
Client: &demo.Client{},
|
||||
},
|
||||
"Demo scheme naming a source that does not exist": {
|
||||
URL: demo.Scheme + "marketing",
|
||||
Err: demo.ErrUnknownSource,
|
||||
},
|
||||
"Demo scheme with no source at all": {
|
||||
URL: demo.Scheme,
|
||||
Err: demo.ErrUnknownSource,
|
||||
},
|
||||
"Real server": {
|
||||
URL: "http://prom.test:9090",
|
||||
Client: &processor.Prometheus{},
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, err := prepRunner(TypePrometheus, c.URL, nil).prometheusClient()
|
||||
testutil.AssertEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
assert.Nil(t, client)
|
||||
return
|
||||
}
|
||||
|
||||
assert.IsType(t, c.Client, client)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
|
||||
gogithub "github.com/google/go-github/v72/github"
|
||||
gogithub "github.com/google/go-github/v88/github"
|
||||
"github.com/guregu/null/v5"
|
||||
"github.com/oxynote/oxynote/server/core/internal/apps/github"
|
||||
"github.com/oxynote/oxynote/server/core/internal/server/internal/apps"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/oxynote/oxynote/server/core/internal/apps/github"
|
||||
"github.com/oxynote/oxynote/server/core/internal/apps/webchange"
|
||||
"github.com/oxynote/oxynote/server/core/internal/datasource"
|
||||
"github.com/oxynote/oxynote/server/core/internal/datasource/demo"
|
||||
"github.com/oxynote/oxynote/server/core/internal/document"
|
||||
"github.com/oxynote/oxynote/server/core/internal/document/hook"
|
||||
"github.com/oxynote/oxynote/server/core/internal/search"
|
||||
|
|
@ -32,14 +33,13 @@ var ErrNoOrganizationMembers = errutil.New(http.StatusBadRequest, "organization.
|
|||
|
||||
// Handler holds dependencies required for organization-related operations.
|
||||
type Handler struct {
|
||||
log *slog.Logger
|
||||
db DB
|
||||
storer Storer
|
||||
githubMan *github.Manager
|
||||
webchangeClient *webchange.Client
|
||||
searchJobs *search.Jobs
|
||||
logoLocation string
|
||||
demoPrometheusURL string
|
||||
log *slog.Logger
|
||||
db DB
|
||||
storer Storer
|
||||
githubMan *github.Manager
|
||||
webchangeClient *webchange.Client
|
||||
searchJobs *search.Jobs
|
||||
logoLocation string
|
||||
}
|
||||
|
||||
// NewHandler creates a new handler instance with the provided logger and database.
|
||||
|
|
@ -51,17 +51,15 @@ func NewHandler(
|
|||
webchangeClient *webchange.Client,
|
||||
searchJobs *search.Jobs,
|
||||
logoLocationFormat string,
|
||||
demoPrometheusURL string,
|
||||
) *Handler {
|
||||
return &Handler{
|
||||
log: log,
|
||||
db: db,
|
||||
storer: storer,
|
||||
githubMan: githubMan,
|
||||
webchangeClient: webchangeClient,
|
||||
searchJobs: searchJobs,
|
||||
logoLocation: logoLocationFormat,
|
||||
demoPrometheusURL: demoPrometheusURL,
|
||||
log: log,
|
||||
db: db,
|
||||
storer: storer,
|
||||
githubMan: githubMan,
|
||||
webchangeClient: webchangeClient,
|
||||
searchJobs: searchJobs,
|
||||
logoLocation: logoLocationFormat,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,19 +71,6 @@ func (h *Handler) InitializeOrganization(w http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
if h.demoPrometheusURL == "" {
|
||||
h.log.Warn("skipping initial demo data source insertion since no URL is configured")
|
||||
|
||||
httpserver.Respond(
|
||||
h.log,
|
||||
w,
|
||||
nil,
|
||||
http.StatusOK,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
members, err := h.db.FetchOrganizationMembers(r.Context(), id)
|
||||
if err != nil {
|
||||
httpserver.RespondError(h.log, w, err)
|
||||
|
|
@ -318,7 +303,7 @@ func (h *Handler) insertDemoDataSource(ctx context.Context, organizationID strin
|
|||
ds := datasource.NewDataSource(datasource.CreateInput{
|
||||
Type: datasource.TypePrometheus,
|
||||
Name: "Demo",
|
||||
URL: h.demoPrometheusURL,
|
||||
URL: demo.URL,
|
||||
Credentials: []byte(`{}`),
|
||||
}, organizationID)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/oxynote/oxynote/server/core/internal/apps/github"
|
||||
"github.com/oxynote/oxynote/server/core/internal/apps/webchange"
|
||||
"github.com/oxynote/oxynote/server/core/internal/datasource"
|
||||
"github.com/oxynote/oxynote/server/core/internal/datasource/demo"
|
||||
"github.com/oxynote/oxynote/server/core/internal/document"
|
||||
"github.com/oxynote/oxynote/server/core/internal/document/hook"
|
||||
"github.com/oxynote/oxynote/server/core/internal/document/hook/processor"
|
||||
|
|
@ -93,7 +94,6 @@ func Test_NewHandler(t *testing.T) {
|
|||
webchangeClient,
|
||||
searchJobs,
|
||||
"loc",
|
||||
"http://prom.test",
|
||||
)
|
||||
require.NotNil(t, hdl)
|
||||
assert.NotNil(t, hdl.log)
|
||||
|
|
@ -103,7 +103,6 @@ func Test_NewHandler(t *testing.T) {
|
|||
assert.Same(t, webchangeClient, hdl.webchangeClient)
|
||||
assert.Same(t, searchJobs, hdl.searchJobs)
|
||||
assert.Equal(t, "loc", hdl.logoLocation)
|
||||
assert.Equal(t, "http://prom.test", hdl.demoPrometheusURL)
|
||||
}
|
||||
|
||||
func Test_Handler_InitializeOrganization(t *testing.T) {
|
||||
|
|
@ -135,7 +134,7 @@ func Test_Handler_InitializeOrganization(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "Demo", ff[0].Ds.Name)
|
||||
assert.Equal(t, datasource.TypePrometheus, ff[0].Ds.Type)
|
||||
assert.Equal(t, "http://prom.test", ff[0].Ds.URL)
|
||||
assert.Equal(t, demo.URL, ff[0].Ds.URL)
|
||||
assert.Equal(t, "org2", ff[0].Ds.OrganizationID)
|
||||
}
|
||||
}
|
||||
|
|
@ -188,7 +187,6 @@ func Test_Handler_InitializeOrganization(t *testing.T) {
|
|||
Members []string
|
||||
MembersErr error
|
||||
BeginErr error
|
||||
NoProm bool
|
||||
OmitID bool
|
||||
Checks []check
|
||||
}{
|
||||
|
|
@ -197,14 +195,6 @@ func Test_Handler_InitializeOrganization(t *testing.T) {
|
|||
OmitID: true,
|
||||
Checks: checks(
|
||||
hasResp(http.StatusNotFound, `{"code":"general","message":"not found"}`),
|
||||
wasInsertDocumentCalled(0),
|
||||
),
|
||||
},
|
||||
"No demo Prometheus URL configured": {
|
||||
Tx: &TxMock{},
|
||||
NoProm: true,
|
||||
Checks: checks(
|
||||
hasResp(http.StatusOK, ""),
|
||||
wasInsertDataSourceCalled(0),
|
||||
wasInsertDocumentCalled(0),
|
||||
),
|
||||
|
|
@ -341,12 +331,6 @@ func Test_Handler_InitializeOrganization(t *testing.T) {
|
|||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
promURL := "http://prom.test"
|
||||
|
||||
if c.NoProm {
|
||||
promURL = ""
|
||||
}
|
||||
|
||||
db := c.DB
|
||||
if db == nil {
|
||||
db = &DBMock{}
|
||||
|
|
@ -357,10 +341,9 @@ func Test_Handler_InitializeOrganization(t *testing.T) {
|
|||
}
|
||||
|
||||
hdl := Handler{
|
||||
log: slog.New(slog.DiscardHandler),
|
||||
db: withTx(db, c.Tx, c.BeginErr),
|
||||
searchJobs: search.NewJobs(true),
|
||||
demoPrometheusURL: promURL,
|
||||
log: slog.New(slog.DiscardHandler),
|
||||
db: withTx(db, c.Tx, c.BeginErr),
|
||||
searchJobs: search.NewJobs(true),
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "http://test.com/", http.NoBody)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ func (h *Handler) handleChannelOpenEvent(ctx context.Context, teamID, userID, ch
|
|||
return fmt.Errorf("failed to get slack client: %w", err)
|
||||
}
|
||||
|
||||
user, err := client.GetUserInfo(userID)
|
||||
user, err := client.GetUserInfoContext(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user info: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
package slack
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
|
@ -355,7 +354,7 @@ func (h *Handler) HandleCommand(w http.ResponseWriter, r *http.Request) {
|
|||
if !app.OrganizationID.Valid { //nolint:nestif // the branching is sequential and readable
|
||||
var user *goslack.User
|
||||
|
||||
user, err = client.GetUserInfo(payload.User.ID)
|
||||
user, err = client.GetUserInfoContext(r.Context(), payload.User.ID)
|
||||
if err != nil {
|
||||
httpserver.RespondError(h.log, w, err)
|
||||
return
|
||||
|
|
@ -429,35 +428,15 @@ func (h *Handler) HandleCommand(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// sendEphemeralResponse sends an ephemeral response to a Slack interaction.
|
||||
func (h *Handler) sendEphemeralResponse(ctx context.Context, responseURL, text string) error {
|
||||
body := struct {
|
||||
Text string `json:"text"`
|
||||
}{
|
||||
Text: text,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal response body: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, responseURL, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send response: %w", err)
|
||||
}
|
||||
defer resp.Body.Close() //nolint:errcheck // error provides no meaningful info
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("received non-OK response: %s", resp.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
return goslack.PostWebhookCustomHTTPContext(
|
||||
ctx,
|
||||
responseURL,
|
||||
h.client,
|
||||
&goslack.WebhookMessage{
|
||||
Text: text,
|
||||
ResponseType: goslack.ResponseTypeEphemeral,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// DB is an interface that handles communication with the document database.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/oxynote/oxynote/server/core/internal/server/internal/auth"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/errutil"
|
||||
"github.com/rs/xid"
|
||||
goslack "github.com/slack-go/slack"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
|
|
@ -124,6 +125,17 @@ func responseServer(t *testing.T) (*httptest.Server, *int) {
|
|||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
ResponseType string `json:"response_type"` //nolint:tagliatelle // slack wire format
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil ||
|
||||
body.ResponseType != goslack.ResponseTypeEphemeral {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hits++
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ func (s *Server) internalRouter() chi.Router {
|
|||
r.Get("/version", s.fetchVersion)
|
||||
r.Mount("/debug", middleware.Profiler())
|
||||
r.Handle("/metrics", promhttp.InstrumentMetricHandler(
|
||||
s.fc, promhttp.HandlerFor(s.fc, promhttp.HandlerOpts{}),
|
||||
s.fc, promhttp.HandlerFor(s.fc, promhttp.HandlerOpts{CoalesceGather: true}),
|
||||
))
|
||||
|
||||
r.Post("/organizations/{organizationId}/initialize", s.handlers.organization.InitializeOrganization)
|
||||
|
|
|
|||
|
|
@ -48,9 +48,6 @@ type Options struct {
|
|||
// PublicURL is the public URL of the server.
|
||||
PublicURL string
|
||||
|
||||
// DemoPrometheusURL is the URL of the demo Prometheus instance.
|
||||
DemoPrometheusURL string
|
||||
|
||||
// Port specifies which port should be used by the server.
|
||||
Port uint
|
||||
|
||||
|
|
@ -172,7 +169,6 @@ func NewServer(
|
|||
webchangeClient,
|
||||
searchJobs,
|
||||
opts.PublicURL+_organizationLogoLocation,
|
||||
opts.DemoPrometheusURL,
|
||||
)
|
||||
srv.handlers.document = document.NewHandler(log, db, githubMan, webchangeClient, searchGateway, searchJobs, notifier, storageClient)
|
||||
srv.handlers.comment = comment.NewHandler(log, db, notifier)
|
||||
|
|
|
|||
|
|
@ -155,11 +155,10 @@ func Test_NewServer(t *testing.T) {
|
|||
srv, err := NewServer(
|
||||
log,
|
||||
Options{
|
||||
PublicURL: "http://test.com",
|
||||
DemoPrometheusURL: "http://prom.test",
|
||||
Port: 8080,
|
||||
Origins: []string{"http://localhost:3000"},
|
||||
Auth: AuthOptions{BetterAuthURL: "http://auth.test"},
|
||||
PublicURL: "http://test.com",
|
||||
Port: 8080,
|
||||
Origins: []string{"http://localhost:3000"},
|
||||
Auth: AuthOptions{BetterAuthURL: "http://auth.test"},
|
||||
MCP: MCPOptions{
|
||||
SessionURL: "http://auth.test/api/internal/mcp/session",
|
||||
ResourceURL: "http://test.com/core/api/mcp",
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ func newFakeS3Server(t *testing.T, f *fakeS3) string {
|
|||
|
||||
// fakeS3 is a minimal in-memory S3 implementation covering only the
|
||||
// requests the minio client performs for the storage package: bucket
|
||||
// location lookup, bucket creation, multipart upload, object stat,
|
||||
// location lookup, bucket creation, object upload, object stat,
|
||||
// object retrieval and object removal.
|
||||
type fakeS3 struct {
|
||||
mu sync.Mutex
|
||||
|
|
@ -176,16 +176,13 @@ type fakeS3 struct {
|
|||
// contentTypes maps completed object keys to their content type.
|
||||
contentTypes map[string]string
|
||||
|
||||
// parts accumulates uploaded multipart data per object key.
|
||||
parts map[string][]byte
|
||||
|
||||
// failBucket forces the bucket existence probe to fail.
|
||||
failBucket bool
|
||||
|
||||
// failMakeBucket forces bucket creation to fail.
|
||||
failMakeBucket bool
|
||||
|
||||
// failUpload forces multipart upload initiation to fail.
|
||||
// failUpload forces object uploads to fail.
|
||||
failUpload bool
|
||||
|
||||
// failStat forces object stat requests to fail.
|
||||
|
|
@ -204,7 +201,7 @@ func (f *fakeS3) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
defer f.mu.Unlock()
|
||||
|
||||
// the maps are initialized independently: a fixture that seeds only
|
||||
// objects still needs the other two.
|
||||
// objects still needs the other one.
|
||||
if f.objects == nil {
|
||||
f.objects = make(map[string][]byte)
|
||||
}
|
||||
|
|
@ -213,10 +210,6 @@ func (f *fakeS3) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
f.contentTypes = make(map[string]string)
|
||||
}
|
||||
|
||||
if f.parts == nil {
|
||||
f.parts = make(map[string][]byte)
|
||||
}
|
||||
|
||||
segments := strings.SplitN(strings.TrimPrefix(r.URL.Path, "/"), "/", 2)
|
||||
|
||||
var key string
|
||||
|
|
@ -234,14 +227,10 @@ func (f *fakeS3) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
f.handleHeadBucket(w)
|
||||
case key == "" && r.Method == http.MethodPut:
|
||||
f.handleMakeBucket(w)
|
||||
case r.Method == http.MethodPost && query.Has("uploads"):
|
||||
f.handleInitiateUpload(w, r, key)
|
||||
case r.Method == http.MethodPut && r.Header.Get("X-Amz-Copy-Source") != "":
|
||||
f.handleCopy(w, r, key)
|
||||
case r.Method == http.MethodPut && query.Get("uploadId") != "":
|
||||
f.handleUploadPart(w, r, key)
|
||||
case r.Method == http.MethodPost && query.Get("uploadId") != "":
|
||||
f.handleCompleteUpload(w, key)
|
||||
case r.Method == http.MethodPut:
|
||||
f.handlePut(w, r, key)
|
||||
case r.Method == http.MethodHead:
|
||||
f.handleStat(w, key)
|
||||
case r.Method == http.MethodGet:
|
||||
|
|
@ -295,46 +284,22 @@ func (f *fakeS3) handleMakeBucket(w http.ResponseWriter) {
|
|||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// handleInitiateUpload serves multipart upload initiation, recording
|
||||
// the object's content type provided by the client.
|
||||
func (f *fakeS3) handleInitiateUpload(w http.ResponseWriter, r *http.Request, key string) {
|
||||
// handlePut stores an object uploaded through a single PUT request,
|
||||
// recording the object's content type provided by the client.
|
||||
func (f *fakeS3) handlePut(w http.ResponseWriter, r *http.Request, key string) {
|
||||
if f.failUpload {
|
||||
writeS3Error(w, http.StatusForbidden, "AccessDenied")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
f.objects[key] = readS3Body(r)
|
||||
f.contentTypes[key] = r.Header.Get("Content-Type")
|
||||
|
||||
fmt.Fprintf( //nolint:errcheck,gosec // test server response errors and input taint are irrelevant
|
||||
w,
|
||||
`<?xml version="1.0"?><InitiateMultipartUploadResult><Key>%s</Key><UploadId>test-upload</UploadId></InitiateMultipartUploadResult>`,
|
||||
key,
|
||||
)
|
||||
}
|
||||
|
||||
// handleUploadPart accumulates a single multipart upload part.
|
||||
func (f *fakeS3) handleUploadPart(w http.ResponseWriter, r *http.Request, key string) {
|
||||
f.parts[key] = append(f.parts[key], readS3Body(r)...)
|
||||
|
||||
w.Header().Set("ETag", `"part-etag"`)
|
||||
w.Header().Set("ETag", `"test-etag"`)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// handleCompleteUpload finalizes a multipart upload by promoting the
|
||||
// accumulated parts to a stored object.
|
||||
func (f *fakeS3) handleCompleteUpload(w http.ResponseWriter, key string) {
|
||||
f.objects[key] = f.parts[key]
|
||||
|
||||
delete(f.parts, key)
|
||||
|
||||
fmt.Fprintf( //nolint:errcheck,gosec // test server response errors and input taint are irrelevant
|
||||
w,
|
||||
`<?xml version="1.0"?><CompleteMultipartUploadResult><Bucket>test-bucket</Bucket><Key>%s</Key><ETag>"test-etag"</ETag></CompleteMultipartUploadResult>`,
|
||||
key,
|
||||
)
|
||||
}
|
||||
|
||||
// handleStat serves object metadata requests.
|
||||
func (f *fakeS3) handleStat(w http.ResponseWriter, key string) {
|
||||
if f.failStat {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"slices"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/errutil"
|
||||
|
|
@ -70,12 +71,23 @@ func (c *Client) Upload(ctx context.Context, folder, id string, r io.Reader) err
|
|||
|
||||
key := path.Join(folder, id)
|
||||
|
||||
// the object is fully buffered rather than streamed: the read side is
|
||||
// already capped at _maxObjectSize, and a seekable reader with an exact
|
||||
// size keeps the client's transient-error retries (disabled for
|
||||
// non-seekable streams) and a single-part upload.
|
||||
rest, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading object: %w", err)
|
||||
}
|
||||
|
||||
data := slices.Concat(buf[:n], rest)
|
||||
|
||||
_, err = c.client.PutObject(
|
||||
ctx,
|
||||
c.bucket,
|
||||
key,
|
||||
io.MultiReader(bytes.NewReader(buf[:n]), r), // Since we read some bytes for content detection, we need to prepend them back.
|
||||
-1,
|
||||
bytes.NewReader(data),
|
||||
int64(len(data)),
|
||||
minio.PutObjectOptions{
|
||||
ContentType: ct,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -63,6 +63,9 @@ func Setup(cfg Config) (func(), error) {
|
|||
Dsn: cfg.DSN,
|
||||
Release: cfg.Release,
|
||||
Environment: cfg.Environment,
|
||||
// nothing in this codebase emits Sentry Logs, so keep the
|
||||
// default-on telemetry machinery switched off.
|
||||
DisableLogs: true,
|
||||
BeforeSend: func(e *sentry.Event, _ *sentry.EventHint) *sentry.Event {
|
||||
// NOTE: Remove default info.
|
||||
e.Modules = nil
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ func Test_Setup(t *testing.T) {
|
|||
closer, err = Setup(Config{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, closer)
|
||||
assert.True(t, sentry.CurrentHub().Client().Options().DisableLogs)
|
||||
assert.NotPanics(t, func() {
|
||||
closer()
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user