add built-in demo prometheus to core

This commit is contained in:
david 2026-08-26 17:00:22 +03:00
parent bd0500c574
commit 996fba144b
67 changed files with 7108 additions and 704 deletions

74
.github/workflows/qa-datagen.yml vendored Normal file
View 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

View File

@ -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

View File

@ -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
View 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$

View File

@ -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

View File

@ -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
View 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())
}

View File

@ -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
)

View File

@ -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=

View File

@ -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,
),
})
}

View 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
}

View File

@ -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 ---

View 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 ""
}

View File

@ -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,

View 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
}
}
}

View File

@ -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))
}

View 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())
}

View File

@ -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 {

View 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
}
}
}

View File

@ -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))

View 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
}

View File

@ -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']

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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,

View File

@ -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
)

View File

@ -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=

View File

@ -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

View File

@ -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 {

View File

@ -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.

View File

@ -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"
)

View File

@ -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.

View File

@ -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",

View File

@ -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
}

View File

@ -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)

View File

@ -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},
},
},
}

View 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
}

View 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)
}

View 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,
)

View 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)
}

View 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
}

View 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())
}

View 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,
}),
},
},
}
}

View 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}))
}

View 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
}

View 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))
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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
}

View File

@ -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.
//

View File

@ -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)
})
}
}

View File

@ -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"

View File

@ -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)

View File

@ -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)

View File

@ -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)
}

View File

@ -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.

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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",

View File

@ -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 {

View File

@ -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,
},

View File

@ -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

View File

@ -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()
})