diff --git a/api/schema.graphqls b/api/schema.graphqls index cf8f5273..8981c859 100644 --- a/api/schema.graphqls +++ b/api/schema.graphqls @@ -250,6 +250,12 @@ type TimeWeights { coreHours: [NullableFloat!]! } +enum ResampleAlgo { + LTTB + AVERAGE + SIMPLE +} + enum Aggregate { USER PROJECT @@ -340,6 +346,7 @@ type Query { metrics: [String!] scopes: [MetricScope!] resolution: Int + resampleAlgo: ResampleAlgo ): [JobMetricWithName!]! jobStats(id: ID!, metrics: [String!]): [NamedStats!]! @@ -399,6 +406,7 @@ type Query { to: Time! page: PageRequest resolution: Int + resampleAlgo: ResampleAlgo ): NodesResultList! clusterMetrics( diff --git a/cmd/cc-backend/init.go b/cmd/cc-backend/init.go index b372dd02..d7bcaf32 100644 --- a/cmd/cc-backend/init.go +++ b/cmd/cc-backend/init.go @@ -24,12 +24,8 @@ const configString = ` "addr": "127.0.0.1:8080", "short-running-jobs-duration": 300, "resampling": { - "minimum-points": 600, - "trigger": 300, - "resolutions": [ - 240, - 60 - ] + "default-policy": "medium", + "default-algo": "lttb" }, "api-allowed-ips": [ "*" diff --git a/cmd/cc-backend/main.go b/cmd/cc-backend/main.go index 9a47b3d7..428c3e7a 100644 --- a/cmd/cc-backend/main.go +++ b/cmd/cc-backend/main.go @@ -91,6 +91,19 @@ func initGops() error { func initConfiguration() error { ccconf.Init(flagConfigFile) + // Warn about unrecognized top-level config sections. A common mistake is + // nesting a setting at the wrong level (e.g. placing "resampling" next to + // "main" instead of inside it), which is otherwise silently ignored. + knownSections := map[string]bool{ + "main": true, "auth": true, "nats": true, "archive": true, + "metric-store": true, "metric-store-external": true, "cron": true, "ui": true, + } + for _, k := range ccconf.GetKeys() { + if !knownSections[k] { + cclog.Warnf("ignoring unrecognized top-level config section %q (check nesting in config file)", k) + } + } + cfg := ccconf.GetPackageConfig("main") if cfg == nil { return fmt.Errorf("main configuration must be present") diff --git a/configs/config.json b/configs/config.json index a0e6ebf6..df85cb3c 100644 --- a/configs/config.json +++ b/configs/config.json @@ -14,9 +14,8 @@ "target-path": "./var/nodestate-archive" }, "resampling": { - "minimum-points": 600, - "trigger": 180, - "resolutions": [240, 60] + "default-policy": "medium", + "default-algo": "lttb" }, "footer-links": { "imprint": "/imprint", diff --git a/go.mod b/go.mod index b3dc1a12..057f2d73 100644 --- a/go.mod +++ b/go.mod @@ -8,17 +8,17 @@ tool ( ) require ( - github.com/99designs/gqlgen v0.17.90 + github.com/99designs/gqlgen v0.17.91 github.com/ClusterCockpit/cc-lib/v2 v2.12.0 github.com/ClusterCockpit/cc-line-protocol/v2 v2.4.0 github.com/Masterminds/squirrel v1.5.4 github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de github.com/alexedwards/scs/v2 v2.9.0 - github.com/aws/aws-sdk-go-v2 v1.41.7 - github.com/aws/aws-sdk-go-v2/config v1.32.18 - github.com/aws/aws-sdk-go-v2/credentials v1.19.17 - github.com/aws/aws-sdk-go-v2/service/s3 v1.102.0 - github.com/coreos/go-oidc/v3 v3.18.0 + github.com/aws/aws-sdk-go-v2 v1.42.0 + github.com/aws/aws-sdk-go-v2/config v1.32.25 + github.com/aws/aws-sdk-go-v2/credentials v1.19.24 + github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0 + github.com/coreos/go-oidc/v3 v3.19.0 github.com/expr-lang/expr v1.17.8 github.com/go-chi/chi/v5 v5.3.0 github.com/go-chi/cors v1.2.2 @@ -28,17 +28,17 @@ require ( github.com/golang-migrate/migrate/v4 v4.19.1 github.com/google/gops v0.3.29 github.com/jmoiron/sqlx v1.4.0 - github.com/mattn/go-sqlite3 v1.14.44 + github.com/mattn/go-sqlite3 v1.14.46 github.com/parquet-go/parquet-go v0.30.1 github.com/qustavo/sqlhooks/v2 v2.1.0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/stretchr/testify v1.11.1 github.com/swaggo/http-swagger v1.3.4 github.com/swaggo/swag v1.16.6 - github.com/vektah/gqlparser/v2 v2.5.33 - golang.org/x/crypto v0.52.0 + github.com/vektah/gqlparser/v2 v2.5.34 + golang.org/x/crypto v0.53.0 golang.org/x/oauth2 v0.36.0 - golang.org/x/sync v0.20.0 + golang.org/x/sync v0.21.0 golang.org/x/time v0.15.0 ) @@ -48,35 +48,36 @@ require ( github.com/agnivade/levenshtein v1.2.1 // indirect github.com/andybalholm/brotli v1.2.1 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.16 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect - github.com/aws/smithy-go v1.26.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect + github.com/aws/smithy-go v1.27.2 // indirect + github.com/coder/websocket v1.8.15 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-openapi/jsonpointer v0.23.1 // indirect - github.com/go-openapi/jsonreference v0.21.5 // indirect - github.com/go-openapi/spec v0.22.4 // indirect - github.com/go-openapi/swag/conv v0.26.0 // indirect - github.com/go-openapi/swag/jsonname v0.26.0 // indirect - github.com/go-openapi/swag/jsonutils v0.26.0 // indirect - github.com/go-openapi/swag/loading v0.26.0 // indirect - github.com/go-openapi/swag/stringutils v0.26.0 // indirect - github.com/go-openapi/swag/typeutils v0.26.0 // indirect - github.com/go-openapi/swag/yamlutils v0.26.0 // indirect + github.com/go-openapi/jsonreference v0.21.6 // indirect + github.com/go-openapi/spec v0.22.6 // indirect + github.com/go-openapi/swag/conv v0.26.1 // indirect + github.com/go-openapi/swag/jsonname v0.26.1 // indirect + github.com/go-openapi/swag/jsonutils v0.26.1 // indirect + github.com/go-openapi/swag/loading v0.26.1 // indirect + github.com/go-openapi/swag/stringutils v0.26.1 // indirect + github.com/go-openapi/swag/typeutils v0.26.1 // indirect + github.com/go-openapi/swag/yamlutils v0.26.1 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/google/uuid v1.6.0 // indirect @@ -90,12 +91,12 @@ require ( github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/nats-io/nats.go v1.52.0 // indirect - github.com/nats-io/nkeys v0.4.15 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/oapi-codegen/runtime v1.4.1 // indirect github.com/parquet-go/bitpack v1.0.0 // indirect github.com/parquet-go/jsonlite v1.5.2 // indirect - github.com/pierrec/lz4/v4 v4.1.26 // indirect + github.com/pierrec/lz4/v4 v4.1.27 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect @@ -106,15 +107,15 @@ require ( github.com/swaggo/files v1.0.1 // indirect github.com/twpayne/go-geom v1.6.1 // indirect github.com/urfave/cli/v2 v2.27.7 // indirect - github.com/urfave/cli/v3 v3.8.0 // indirect + github.com/urfave/cli/v3 v3.9.0 // indirect github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.36.0 // indirect - golang.org/x/net v0.55.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/text v0.37.0 // indirect - golang.org/x/tools v0.45.0 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.38.0 // indirect + golang.org/x/tools v0.46.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/go.sum b/go.sum index d5689234..ec53b4c9 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/99designs/gqlgen v0.17.90 h1:wSv6blm/PoplU6QoNw83EcQpNtC0HX3/+44vITJOzpk= -github.com/99designs/gqlgen v0.17.90/go.mod h1:GqYrEwYsqCG8VaOsq2kJUCUKwAE1T+u2i+Nj7NtXiVI= +github.com/99designs/gqlgen v0.17.91 h1:/mIvXnN0lAorqszP3Vukw10SVRfLVUYtBTQFwmYRMmI= +github.com/99designs/gqlgen v0.17.91/go.mod h1:N7+yJF6zbGIEqohF+ZtEUp/eq2dTnn0bDizLUIYPUCU= github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw= github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/ClusterCockpit/cc-lib/v2 v2.12.0 h1:ZbGD68nDniuvzFjJCdyYawpCBrabdSyWOg5FFSyFbjQ= @@ -16,8 +16,6 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8 github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/NVIDIA/go-nvml v0.13.0-1 h1:OLX8Jq3dONuPOQPC7rndB6+iDmDakw0XTYgzMxObkEw= github.com/NVIDIA/go-nvml v0.13.0-1/go.mod h1:+KNA7c7gIBH7SKSJ1ntlwkfN80zdx8ovl4hrK3LmPt4= -github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo= -github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= @@ -31,63 +29,59 @@ github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de h1:c7 github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss= github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90= github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= -github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0mDD1gk7o9BhI16b9p5yYAXRlidpqJE= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= -github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= -github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY= -github.com/aws/aws-sdk-go-v2/config v1.32.18 h1:Hcia46bxhGgF3BaSnG8nSNCWmqTK6bj9xN9/FJ3WK6Q= -github.com/aws/aws-sdk-go-v2/config v1.32.18/go.mod h1:zEjCAYmxqDadH1WX8CdBvmLKhUEUVFgKRQG38zjDmrY= -github.com/aws/aws-sdk-go-v2/credentials v1.19.17 h1:gP2nkGsS+KMvF/jfFz2Vv2qiiOqWKyPACSzPsqHgoW8= -github.com/aws/aws-sdk-go-v2/credentials v1.19.17/go.mod h1:Bsew3S/moG5iT77giPj1q8wb/s0RE5/QfH+ASjYtuQc= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.16 h1:tX68nPDCoX0s2ksM7CipWP0QFw2hGDWwUdxI6+eT9ZU= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.16/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.102.0 h1:gfPQ6do5PZTCc5n/vZUHz/G8McrNrfERGSO+iHvVbCA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.102.0/go.mod h1:wO6U9egJtCtsZEHG2AAcFf1kUWDRrH0Iif6K3bVmmdE= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0 h1:nDARhv/oF55bcxF7rCI/4PDxOKnVXVWwDuDwCs2I2SQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg= -github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk= -github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio= -github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s= -github.com/aws/smithy-go v1.26.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= +github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls= +github.com/aws/aws-sdk-go-v2/config v1.32.25 h1:ACCejvStYoilgwrfegSt5ZntCbPrk52qfwyNcnl3omM= +github.com/aws/aws-sdk-go-v2/config v1.32.25/go.mod h1:LJyU8sDRbXUxFn8xMJIGP+v9QYYwveNLI8a/giAOiAs= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 h1:V51LGlOq/1VsDsHUdoklAQi7rMmx4qQubvFYAlP2254= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22/go.mod h1:4Pzhyz8hJOm2bepgl+NjvRx8vlUFAIIvJnZ/MkcNPpU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 h1:hiME6pBzC7OTl9LMtlyTWBuEl1f4QBcUmFDKC7MLXtc= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29/go.mod h1:G7RP+uhagpKtKhd1BM9N6JQqjCcGEU47K5lBVZQyRQw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0 h1:ta8csKy5vN91F3i5gGR85lFV0srBqySEji7Jroes6rE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0/go.mod h1:77ZAgynvx1txMvDG8gGWoWkO1augYDxkp9JElWFgjQU= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 h1:3nXpRcFwRCW8n7HgO2QGy0Dc20eQNfBuUemGQhpF8m8= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc= +github.com/aws/smithy-go v1.27.2 h1:y9NPmSE6am6LjEFPfqHqG/jJk7AauQvhCJONKh7kpzk= +github.com/aws/smithy-go v1.27.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= 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/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= 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/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= -github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= -github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A= -github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= +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/coreos/go-oidc/v3 v3.19.0 h1:F/xyOi3x1UnG1U27YVnM1N6bHiL1K2upi6U/0qr8r+I= +github.com/coreos/go-oidc/v3 v3.19.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -117,31 +111,31 @@ github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baD github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0= github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= -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/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= -github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= +github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y= +github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY= +github.com/go-openapi/spec v0.22.6 h1:Tyy1pLaNCM8GBCFLoGYLonjJi6zykqyLCjXLc19ZPic= +github.com/go-openapi/spec v0.22.6/go.mod h1:HZvTHat+iH0PALQRWhrqIHtU/PEqxqd89fu0MxGlMeM= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= -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/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= -github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= -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/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= -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/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-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0= -github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE= -github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= -github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-openapi/swag/conv v0.26.1 h1:slr5FVkg9Wc3Y5zcwenD8Sd/PQ94b2I/QJI7N7KTBpg= +github.com/go-openapi/swag/conv v0.26.1/go.mod h1:mvQXgPptZk9GTrFgGwWvT4q+dN+zQej9JfmGwnipz1A= +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.1 h1:2hdBfFkHg+7Wrz2VsCbeyR6hzkRDs7AztnMR2u84yOY= +github.com/go-openapi/swag/jsonutils v0.26.1/go.mod h1:U+RMJH3wa+6BRiphuRtIyI8fW9HPFqFQ4sHk2oRx0UQ= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1 h1:1CD7NiLLb/TXl3tOnFYU4b+mNfb5rtgHkaA+q7RMYYQ= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1/go.mod h1:ZWafc8nMdYzTE3uYY6W86f0n46+IF0g4uUyRhJw/kXc= +github.com/go-openapi/swag/loading v0.26.1 h1:E9K4wqXeROlhjFQ13K9zMz6ojFGXIggGe+ad1odrK9w= +github.com/go-openapi/swag/loading v0.26.1/go.mod h1:3qvRIlWzWdq1HvmldwmuJ2ohpcAryN6xVt2OTKd0/7E= +github.com/go-openapi/swag/stringutils v0.26.1 h1:f88uYyTso7TnHrKM/bUBsQ5e2wKf37cpgo6pvbzd9yU= +github.com/go-openapi/swag/stringutils v0.26.1/go.mod h1:Sc6d3bU8fgk5AyZR8/8jEQ+Is/Ald+TD/IIggPN8UJk= +github.com/go-openapi/swag/typeutils v0.26.1 h1:yg42FgMzRR6PVQ3M3qHz1s+Y6/P4HoJ3cBarXa3OVnU= +github.com/go-openapi/swag/typeutils v0.26.1/go.mod h1:VfnV+oUtSP2vCSCn2aJgnr8OevUYemyIzzS1VOzS10o= +github.com/go-openapi/swag/yamlutils v0.26.1 h1:0TSLK+lXs9vfIhAWzBeI/lOzEnIoot6WTCO1aAeWFTk= +github.com/go-openapi/swag/yamlutils v0.26.1/go.mod h1:7W5b7PRX9MxwL7TjeG7H8HkyBGRsIDRObhyMWFgBI2M= +github.com/go-openapi/testify/enable/yaml/v2 v2.5.1 h1:q9NtHwK4qHF7yZziBPvZyv7zWAIk8ok88Gh2mR6Jpc8= +github.com/go-openapi/testify/enable/yaml/v2 v2.5.1/go.mod h1:JW0MXIotCYps/XsgJnG3a8Q7rE5xAiBwoOD5OfaIQBk= +github.com/go-openapi/testify/v2 v2.5.1 h1:TMdhCaw8fUNraVSf3Omoob1dO/AzBfhtFAPW0an6sBo= +github.com/go-openapi/testify/v2 v2.5.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= @@ -210,8 +204,8 @@ github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8= -github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= +github.com/mattn/go-sqlite3 v1.14.46 h1:ZfaNcYO/CGNMRxkN1vvG9qf+Y+uvXfgT9a6MlEw+HmU= +github.com/mattn/go-sqlite3 v1.14.46/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -222,8 +216,8 @@ github.com/nats-io/nats-server/v2 v2.12.7 h1:prQ9cPiWHcnwfT81Wi5lU9LL8TLY+7pxDru github.com/nats-io/nats-server/v2 v2.12.7/go.mod h1:dOnmkprKMluTmTF7/QHZioxlau3sKHUM/LBPy9AiBPw= github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc= github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= -github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= -github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs= @@ -237,8 +231,8 @@ github.com/parquet-go/jsonlite v1.5.2 h1:8TZzYknFOHUpgjTLf80qbzc+8GdeT/3a3fdXSzh github.com/parquet-go/jsonlite v1.5.2/go.mod h1:nDjpkpL4EOtqs6NQugUsi0Rleq9sW/OtC1NnZEnxzF0= github.com/parquet-go/parquet-go v0.30.1 h1:Oy6ganNrAdFiVwy7wNmWagfPTWA2X9Z3tVHBc7JtuX8= github.com/parquet-go/parquet-go v0.30.1/go.mod h1:navtkAYr2LGoJVp141oXPlO/sxLvaOe3la2JEoD8+rg= -github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= -github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk= +github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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= @@ -264,8 +258,6 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE= github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= @@ -289,10 +281,10 @@ github.com/twpayne/go-geom v1.6.1 h1:iLE+Opv0Ihm/ABIcvQFGIiFBXd76oBIar9drAwHFhR4 github.com/twpayne/go-geom v1.6.1/go.mod h1:Kr+Nly6BswFsKM5sd31YaoWS5PeDDH2NftJTK7Gd028= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= -github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI= -github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= -github.com/vektah/gqlparser/v2 v2.5.33 h1:lRp8aIeNUNbimf/axZd7ETg24q06hBtPaas+TcvI/7E= -github.com/vektah/gqlparser/v2 v2.5.33/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= +github.com/urfave/cli/v3 v3.9.0 h1:AV9lIiPv3ukYnxunaCUsHnEozptYmDN2F0+yWqLMn/c= +github.com/urfave/cli/v3 v3.9.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +github.com/vektah/gqlparser/v2 v2.5.34 h1:MEea5P0qhdcqfBL45ghKE+qr9laidVHTMHjav5h7ckk= +github.com/vektah/gqlparser/v2 v2.5.34/go.mod h1:mFdHLGCio7OGX1fby9ZjTW6FN+qxgmbnBcRIeeScE5s= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= @@ -306,31 +298,31 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +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.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -338,15 +330,15 @@ 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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= 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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= +golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/internal/api/api_test.go b/internal/api/api_test.go index a8aef889..ec3f55ff 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -355,7 +355,7 @@ func TestRestApi(t *testing.T) { } t.Run("CheckArchive", func(t *testing.T) { - data, err := metricdispatch.LoadData(stoppedJob, []string{"load_one"}, []schema.MetricScope{schema.MetricScopeNode}, context.Background(), 60) + data, err := metricdispatch.LoadData(stoppedJob, []string{"load_one"}, []schema.MetricScope{schema.MetricScopeNode}, context.Background(), 60, "") if err != nil { t.Fatal(err) } diff --git a/internal/api/job.go b/internal/api/job.go index 37c0e454..b5695f7a 100644 --- a/internal/api/job.go +++ b/internal/api/job.go @@ -309,7 +309,7 @@ func (api *RestAPI) getCompleteJobByID(rw http.ResponseWriter, r *http.Request) } if r.URL.Query().Get("all-metrics") == "true" { - data, err = metricdispatch.LoadData(job, nil, scopes, r.Context(), resolution) + data, err = metricdispatch.LoadData(job, nil, scopes, r.Context(), resolution, "") if err != nil { cclog.Warnf("REST: error while loading all-metrics job data for JobID %d on %s", job.JobID, job.Cluster) return @@ -405,7 +405,7 @@ func (api *RestAPI) getJobByID(rw http.ResponseWriter, r *http.Request) { resolution = max(resolution, mc.Timestep) } - data, err := metricdispatch.LoadData(job, metrics, scopes, r.Context(), resolution) + data, err := metricdispatch.LoadData(job, metrics, scopes, r.Context(), resolution, "") if err != nil { cclog.Warnf("REST: error while loading job data for JobID %d on %s", job.JobID, job.Cluster) return @@ -1086,7 +1086,7 @@ func (api *RestAPI) getJobMetrics(rw http.ResponseWriter, r *http.Request) { } resolver := graph.GetResolverInstance() - data, err := resolver.Query().JobMetrics(r.Context(), id, metrics, scopes, nil) + data, err := resolver.Query().JobMetrics(r.Context(), id, metrics, scopes, nil, nil) if err != nil { if err := json.NewEncoder(rw).Encode(Response{ Error: &struct { diff --git a/internal/archiver/archiver.go b/internal/archiver/archiver.go index 454a2358..480972fd 100644 --- a/internal/archiver/archiver.go +++ b/internal/archiver/archiver.go @@ -59,7 +59,7 @@ func ArchiveJob(job *schema.Job, ctx context.Context) (*schema.Job, error) { scopes = append(scopes, schema.MetricScopeAccelerator) } - jobData, err := metricdispatch.LoadData(job, allMetrics, scopes, ctx, 0) // 0 Resulotion-Value retrieves highest res (60s) + jobData, err := metricdispatch.LoadData(job, allMetrics, scopes, ctx, 0, "") // 0 Resulotion-Value retrieves highest res (60s) if err != nil { cclog.Error("Error wile loading job data for archiving") return nil, err diff --git a/internal/config/config.go b/internal/config/config.go index 45c01b57..83322403 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -118,12 +118,12 @@ type NodeStateRetention struct { } type ResampleConfig struct { - // Minimum number of points to trigger resampling of data - MinimumPoints int `json:"minimum-points"` - // Array of resampling target resolutions, in seconds; Example: [600,300,60] - Resolutions []int `json:"resolutions"` - // Trigger next zoom level at less than this many visible datapoints - Trigger int `json:"trigger"` + // Default resample policy when no user preference is set ("low", "medium", "high") + DefaultPolicy string `json:"default-policy"` + // Default resample algorithm when no user preference is set ("lttb", "average", "simple") + DefaultAlgo string `json:"default-algo"` + // Policy-derived target point count (set dynamically from user preference, not from config.json) + TargetPoints int `json:"targetPoints,omitempty"` } type NATSConfig struct { @@ -171,7 +171,24 @@ func Init(mainConfig json.RawMessage) { cclog.Abortf("Config Init: Could not decode config file '%s'.\nError: %s\n", mainConfig, err.Error()) } - if Keys.EnableResampling != nil && Keys.EnableResampling.MinimumPoints > 0 { - resampler.SetMinimumRequiredPoints(Keys.EnableResampling.MinimumPoints) + if Keys.EnableResampling != nil { + policy := Keys.EnableResampling.DefaultPolicy + if policy == "" { + policy = "medium" + } + resampler.SetMinimumRequiredPoints(targetPointsForPolicy(policy)) + } +} + +func targetPointsForPolicy(policy string) int { + switch policy { + case "low": + return 200 + case "medium": + return 500 + case "high": + return 1000 + default: + return 500 } } diff --git a/internal/config/schema.go b/internal/config/schema.go index d447db1f..7a1ecb2a 100644 --- a/internal/config/schema.go +++ b/internal/config/schema.go @@ -92,23 +92,17 @@ var configSchema = ` "description": "Enable dynamic zoom in frontend metric plots.", "type": "object", "properties": { - "minimum-points": { - "description": "Minimum points to trigger resampling of time-series data.", - "type": "integer" - }, - "trigger": { - "description": "Trigger next zoom level at less than this many visible datapoints.", - "type": "integer" + "default-policy": { + "description": "Default resample policy when no user preference is set.", + "type": "string", + "enum": ["low", "medium", "high"] }, - "resolutions": { - "description": "Array of resampling target resolutions, in seconds.", - "type": "array", - "items": { - "type": "integer" - } + "default-algo": { + "description": "Default resample algorithm when no user preference is set.", + "type": "string", + "enum": ["lttb", "average", "simple"] } - }, - "required": ["trigger", "resolutions"] + } }, "api-subjects": { "description": "NATS subjects configuration for subscribing to job and node events.", diff --git a/internal/graph/generated/generated.go b/internal/graph/generated/generated.go index 22aaafac..1e54dd20 100644 --- a/internal/graph/generated/generated.go +++ b/internal/graph/generated/generated.go @@ -327,7 +327,7 @@ type ComplexityRoot struct { Clusters func(childComplexity int) int GlobalMetrics func(childComplexity int) int Job func(childComplexity int, id string) int - JobMetrics func(childComplexity int, id string, metrics []string, scopes []schema.MetricScope, resolution *int) int + JobMetrics func(childComplexity int, id string, metrics []string, scopes []schema.MetricScope, resolution *int, resampleAlgo *model.ResampleAlgo) int JobStats func(childComplexity int, id string, metrics []string) int Jobs func(childComplexity int, filter []*model.JobFilter, page *model.PageRequest, order *model.OrderByInput) int JobsFootprints func(childComplexity int, filter []*model.JobFilter, metrics []string) int @@ -335,7 +335,7 @@ type ComplexityRoot struct { JobsStatistics func(childComplexity int, filter []*model.JobFilter, metrics []string, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate, numDurationBins *string, numMetricBins *int) int Node func(childComplexity int, id string) int NodeMetrics func(childComplexity int, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) int - NodeMetricsList func(childComplexity int, cluster string, subCluster string, stateFilter string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int) int + NodeMetricsList func(childComplexity int, cluster string, subCluster string, stateFilter string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int, resampleAlgo *model.ResampleAlgo) int NodeStates func(childComplexity int, filter []*model.NodeFilter) int NodeStatesTimed func(childComplexity int, filter []*model.NodeFilter, typeArg string) int Nodes func(childComplexity int, filter []*model.NodeFilter, order *model.OrderByInput) int @@ -483,7 +483,7 @@ type QueryResolver interface { NodeStates(ctx context.Context, filter []*model.NodeFilter) ([]*model.NodeStates, error) NodeStatesTimed(ctx context.Context, filter []*model.NodeFilter, typeArg string) ([]*model.NodeStatesTimed, error) Job(ctx context.Context, id string) (*schema.Job, error) - JobMetrics(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope, resolution *int) ([]*model.JobMetricWithName, error) + JobMetrics(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope, resolution *int, resampleAlgo *model.ResampleAlgo) ([]*model.JobMetricWithName, error) JobStats(ctx context.Context, id string, metrics []string) ([]*model.NamedStats, error) ScopedJobStats(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope) ([]*model.NamedStatsWithScope, error) Jobs(ctx context.Context, filter []*model.JobFilter, page *model.PageRequest, order *model.OrderByInput) (*model.JobResultList, error) @@ -492,7 +492,7 @@ type QueryResolver interface { JobsFootprints(ctx context.Context, filter []*model.JobFilter, metrics []string) (*model.Footprints, error) RooflineHeatmap(ctx context.Context, filter []*model.JobFilter, rows int, cols int, minX float64, minY float64, maxX float64, maxY float64) ([][]float64, error) NodeMetrics(ctx context.Context, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) ([]*model.NodeMetrics, error) - NodeMetricsList(ctx context.Context, cluster string, subCluster string, stateFilter string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int) (*model.NodesResultList, error) + NodeMetricsList(ctx context.Context, cluster string, subCluster string, stateFilter string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int, resampleAlgo *model.ResampleAlgo) (*model.NodesResultList, error) ClusterMetrics(ctx context.Context, cluster string, metrics []string, from time.Time, to time.Time) (*model.ClusterMetrics, error) } type SubClusterResolver interface { @@ -1666,7 +1666,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.ComplexityRoot.Query.JobMetrics(childComplexity, args["id"].(string), args["metrics"].([]string), args["scopes"].([]schema.MetricScope), args["resolution"].(*int)), true + return e.ComplexityRoot.Query.JobMetrics(childComplexity, args["id"].(string), args["metrics"].([]string), args["scopes"].([]schema.MetricScope), args["resolution"].(*int), args["resampleAlgo"].(*model.ResampleAlgo)), true case "Query.jobStats": if e.ComplexityRoot.Query.JobStats == nil { break @@ -1754,7 +1754,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.ComplexityRoot.Query.NodeMetricsList(childComplexity, args["cluster"].(string), args["subCluster"].(string), args["stateFilter"].(string), args["nodeFilter"].(string), args["scopes"].([]schema.MetricScope), args["metrics"].([]string), args["from"].(time.Time), args["to"].(time.Time), args["page"].(*model.PageRequest), args["resolution"].(*int)), true + return e.ComplexityRoot.Query.NodeMetricsList(childComplexity, args["cluster"].(string), args["subCluster"].(string), args["stateFilter"].(string), args["nodeFilter"].(string), args["scopes"].([]schema.MetricScope), args["metrics"].([]string), args["from"].(time.Time), args["to"].(time.Time), args["page"].(*model.PageRequest), args["resolution"].(*int), args["resampleAlgo"].(*model.ResampleAlgo)), true case "Query.nodeStates": if e.ComplexityRoot.Query.NodeStates == nil { break @@ -2525,6 +2525,12 @@ type TimeWeights { coreHours: [NullableFloat!]! } +enum ResampleAlgo { + LTTB + AVERAGE + SIMPLE +} + enum Aggregate { USER PROJECT @@ -2615,6 +2621,7 @@ type Query { metrics: [String!] scopes: [MetricScope!] resolution: Int + resampleAlgo: ResampleAlgo ): [JobMetricWithName!]! jobStats(id: ID!, metrics: [String!]): [NamedStats!]! @@ -2674,6 +2681,7 @@ type Query { to: Time! page: PageRequest resolution: Int + resampleAlgo: ResampleAlgo ): NodesResultList! clusterMetrics( @@ -3882,6 +3890,11 @@ func (ec *executionContext) field_Query_jobMetrics_args(ctx context.Context, raw return nil, err } args["resolution"] = arg3 + arg4, err := graphql.ProcessArgField(ctx, rawArgs, "resampleAlgo", ec.unmarshalOResampleAlgo2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐResampleAlgo) + if err != nil { + return nil, err + } + args["resampleAlgo"] = arg4 return args, nil } @@ -4140,6 +4153,11 @@ func (ec *executionContext) field_Query_nodeMetricsList_args(ctx context.Context return nil, err } args["resolution"] = arg9 + arg10, err := graphql.ProcessArgField(ctx, rawArgs, "resampleAlgo", ec.unmarshalOResampleAlgo2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐResampleAlgo) + if err != nil { + return nil, err + } + args["resampleAlgo"] = arg10 return args, nil } @@ -9307,7 +9325,7 @@ func (ec *executionContext) _Query_jobMetrics(ctx context.Context, field graphql }, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.Resolvers.Query().JobMetrics(ctx, fc.Args["id"].(string), fc.Args["metrics"].([]string), fc.Args["scopes"].([]schema.MetricScope), fc.Args["resolution"].(*int)) + return ec.Resolvers.Query().JobMetrics(ctx, fc.Args["id"].(string), fc.Args["metrics"].([]string), fc.Args["scopes"].([]schema.MetricScope), fc.Args["resolution"].(*int), fc.Args["resampleAlgo"].(*model.ResampleAlgo)) }, nil, func(ctx context.Context, selections ast.SelectionSet, v []*model.JobMetricWithName) graphql.Marshaler { @@ -9703,7 +9721,7 @@ func (ec *executionContext) _Query_nodeMetricsList(ctx context.Context, field gr }, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.Resolvers.Query().NodeMetricsList(ctx, fc.Args["cluster"].(string), fc.Args["subCluster"].(string), fc.Args["stateFilter"].(string), fc.Args["nodeFilter"].(string), fc.Args["scopes"].([]schema.MetricScope), fc.Args["metrics"].([]string), fc.Args["from"].(time.Time), fc.Args["to"].(time.Time), fc.Args["page"].(*model.PageRequest), fc.Args["resolution"].(*int)) + return ec.Resolvers.Query().NodeMetricsList(ctx, fc.Args["cluster"].(string), fc.Args["subCluster"].(string), fc.Args["stateFilter"].(string), fc.Args["nodeFilter"].(string), fc.Args["scopes"].([]schema.MetricScope), fc.Args["metrics"].([]string), fc.Args["from"].(time.Time), fc.Args["to"].(time.Time), fc.Args["page"].(*model.PageRequest), fc.Args["resolution"].(*int), fc.Args["resampleAlgo"].(*model.ResampleAlgo)) }, nil, func(ctx context.Context, selections ast.SelectionSet, v *model.NodesResultList) graphql.Marshaler { @@ -18679,6 +18697,22 @@ func (ec *executionContext) unmarshalOPageRequest2ᚖgithubᚗcomᚋClusterCockp return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalOResampleAlgo2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐResampleAlgo(ctx context.Context, v any) (*model.ResampleAlgo, error) { + if v == nil { + return nil, nil + } + var res = new(model.ResampleAlgo) + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOResampleAlgo2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐResampleAlgo(ctx context.Context, sel ast.SelectionSet, v *model.ResampleAlgo) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return v +} + func (ec *executionContext) unmarshalOSchedulerState2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑlibᚋv2ᚋschemaᚐSchedulerState(ctx context.Context, v any) (*schema.SchedulerState, error) { if v == nil { return nil, nil diff --git a/internal/graph/model/models_gen.go b/internal/graph/model/models_gen.go index 7611bf22..f59d6a2a 100644 --- a/internal/graph/model/models_gen.go +++ b/internal/graph/model/models_gen.go @@ -328,6 +328,63 @@ func (e Aggregate) MarshalJSON() ([]byte, error) { return buf.Bytes(), nil } +type ResampleAlgo string + +const ( + ResampleAlgoLttb ResampleAlgo = "LTTB" + ResampleAlgoAverage ResampleAlgo = "AVERAGE" + ResampleAlgoSimple ResampleAlgo = "SIMPLE" +) + +var AllResampleAlgo = []ResampleAlgo{ + ResampleAlgoLttb, + ResampleAlgoAverage, + ResampleAlgoSimple, +} + +func (e ResampleAlgo) IsValid() bool { + switch e { + case ResampleAlgoLttb, ResampleAlgoAverage, ResampleAlgoSimple: + return true + } + return false +} + +func (e ResampleAlgo) String() string { + return string(e) +} + +func (e *ResampleAlgo) UnmarshalGQL(v any) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = ResampleAlgo(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid ResampleAlgo", str) + } + return nil +} + +func (e ResampleAlgo) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +func (e *ResampleAlgo) UnmarshalJSON(b []byte) error { + s, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + return e.UnmarshalGQL(s) +} + +func (e ResampleAlgo) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + e.MarshalGQL(&buf) + return buf.Bytes(), nil +} + type SortByAggregate string const ( diff --git a/internal/graph/resample.go b/internal/graph/resample.go new file mode 100644 index 00000000..a1b0406f --- /dev/null +++ b/internal/graph/resample.go @@ -0,0 +1,145 @@ +// Copyright (C) NHR@FAU, University Erlangen-Nuremberg. +// All rights reserved. This file is part of cc-backend. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package graph + +import ( + "context" + "strings" + + "github.com/ClusterCockpit/cc-backend/internal/config" + "github.com/ClusterCockpit/cc-backend/internal/graph/model" + "github.com/ClusterCockpit/cc-backend/internal/metricdispatch" + "github.com/ClusterCockpit/cc-backend/internal/repository" + "github.com/ClusterCockpit/cc-backend/pkg/archive" +) + +// resolveResolutionFromPolicy reads the user's resample policy preference and +// computes a resolution based on job duration and metric frequency. Returns nil +// if the user has no policy set. +func resolveResolutionFromPolicy(ctx context.Context, duration int64, cluster string, metrics []string) *int { + user := repository.GetUserFromContext(ctx) + if user == nil { + return nil + } + + conf, err := repository.GetUserCfgRepo().GetUIConfig(user) + if err != nil { + return nil + } + + policyVal, ok := conf["plotConfiguration_resamplePolicy"] + if !ok { + return nil + } + policyStr, ok := policyVal.(string) + if !ok || policyStr == "" { + return nil + } + + policy := metricdispatch.ResamplePolicy(policyStr) + targetPoints := metricdispatch.TargetPointsForPolicy(policy) + if targetPoints == 0 { + return nil + } + + // Find the smallest metric frequency across the requested metrics + frequency := smallestFrequency(cluster, metrics) + if frequency <= 0 { + return nil + } + + res := metricdispatch.ComputeResolution(duration, int64(frequency), targetPoints) + return &res +} + +// resolveResampleAlgo returns the resampling algorithm name to use, checking +// the explicit GraphQL parameter first, then the user's preference. +func resolveResampleAlgo(ctx context.Context, resampleAlgo *model.ResampleAlgo) string { + if resampleAlgo != nil { + return strings.ToLower(resampleAlgo.String()) + } + + user := repository.GetUserFromContext(ctx) + if user == nil { + return "" + } + + conf, err := repository.GetUserCfgRepo().GetUIConfig(user) + if err != nil { + return "" + } + + algoVal, ok := conf["plotConfiguration_resampleAlgo"] + if ok { + if algoStr, ok := algoVal.(string); ok && algoStr != "" { + return algoStr + } + } + + // Fall back to global default algo + if config.Keys.EnableResampling != nil && config.Keys.EnableResampling.DefaultAlgo != "" { + return config.Keys.EnableResampling.DefaultAlgo + } + + return "" +} + +// resolveResolutionFromDefaultPolicy computes a resolution using the global +// default policy from config. Returns nil if no policy is configured. +func resolveResolutionFromDefaultPolicy(duration int64, cluster string, metrics []string) *int { + cfg := config.Keys.EnableResampling + if cfg == nil { + return nil + } + + policyStr := cfg.DefaultPolicy + if policyStr == "" { + policyStr = "medium" + } + + policy := metricdispatch.ResamplePolicy(policyStr) + targetPoints := metricdispatch.TargetPointsForPolicy(policy) + if targetPoints == 0 { + return nil + } + + frequency := smallestFrequency(cluster, metrics) + if frequency <= 0 { + return nil + } + + res := metricdispatch.ComputeResolution(duration, int64(frequency), targetPoints) + return &res +} + +// smallestFrequency returns the smallest metric timestep (in seconds) among the +// requested metrics for the given cluster. Falls back to 0 if nothing is found. +func smallestFrequency(cluster string, metrics []string) int { + cl := archive.GetCluster(cluster) + if cl == nil { + return 0 + } + + minFreq := 0 + for _, mc := range cl.MetricConfig { + if len(metrics) > 0 { + found := false + for _, m := range metrics { + if mc.Name == m { + found = true + break + } + } + if !found { + continue + } + } + if minFreq == 0 || mc.Timestep < minFreq { + minFreq = mc.Timestep + } + } + + return minFreq +} diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index 3ef58ad4..b8c65bcf 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -498,24 +498,30 @@ func (r *queryResolver) Job(ctx context.Context, id string) (*schema.Job, error) } // JobMetrics is the resolver for the jobMetrics field. -func (r *queryResolver) JobMetrics(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope, resolution *int) ([]*model.JobMetricWithName, error) { - if resolution == nil { // Load from Config +func (r *queryResolver) JobMetrics(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope, resolution *int, resampleAlgo *model.ResampleAlgo) ([]*model.JobMetricWithName, error) { + job, err := r.Query().Job(ctx, id) + if err != nil { + cclog.Warn("Error while querying job for metrics") + return nil, err + } + + // Resolve resolution: explicit param > user policy > global config > 0 + if resolution == nil { + resolution = resolveResolutionFromPolicy(ctx, int64(job.Duration), job.Cluster, metrics) + } + if resolution == nil { if config.Keys.EnableResampling != nil { - defaultRes := slices.Max(config.Keys.EnableResampling.Resolutions) - resolution = &defaultRes - } else { // Set 0 (Loads configured metric timestep) + resolution = resolveResolutionFromDefaultPolicy(int64(job.Duration), job.Cluster, metrics) + } + if resolution == nil { defaultRes := 0 resolution = &defaultRes } } - job, err := r.Query().Job(ctx, id) - if err != nil { - cclog.Warn("Error while querying job for metrics") - return nil, err - } + algoName := resolveResampleAlgo(ctx, resampleAlgo) - data, err := metricdispatch.LoadData(job, metrics, scopes, ctx, *resolution) + data, err := metricdispatch.LoadData(job, metrics, scopes, ctx, *resolution, algoName) if err != nil { cclog.Warn("Error while loading job data") return nil, err @@ -877,12 +883,17 @@ func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes [ } // NodeMetricsList is the resolver for the nodeMetricsList field. -func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, subCluster string, stateFilter string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int) (*model.NodesResultList, error) { - if resolution == nil { // Load from Config +func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, subCluster string, stateFilter string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int, resampleAlgo *model.ResampleAlgo) (*model.NodesResultList, error) { + // Resolve resolution: explicit param > user policy > global config > 0 + duration := int64(to.Sub(from).Seconds()) + if resolution == nil { + resolution = resolveResolutionFromPolicy(ctx, duration, cluster, metrics) + } + if resolution == nil { if config.Keys.EnableResampling != nil { - defaultRes := slices.Max(config.Keys.EnableResampling.Resolutions) - resolution = &defaultRes - } else { // Set 0 (Loads configured metric timestep) + resolution = resolveResolutionFromDefaultPolicy(duration, cluster, metrics) + } + if resolution == nil { defaultRes := 0 resolution = &defaultRes } @@ -906,8 +917,10 @@ func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, sub } } + algoName := resolveResampleAlgo(ctx, resampleAlgo) + // data -> map hostname:jobdata - data, err := metricdispatch.LoadNodeListData(cluster, subCluster, nodes, metrics, scopes, *resolution, from, to, ctx) + data, err := metricdispatch.LoadNodeListData(cluster, subCluster, nodes, metrics, scopes, *resolution, from, to, ctx, algoName) if err != nil { cclog.Warn("error while loading node data (Resolver.NodeMetricsList") return nil, err diff --git a/internal/graph/util.go b/internal/graph/util.go index 5458d0ff..7c0c0346 100644 --- a/internal/graph/util.go +++ b/internal/graph/util.go @@ -55,7 +55,7 @@ func (r *queryResolver) rooflineHeatmap( // resolution = max(resolution, mc.Timestep) // } - jobdata, err := metricdispatch.LoadData(job, []string{"flops_any", "mem_bw"}, []schema.MetricScope{schema.MetricScopeNode}, ctx, 0) + jobdata, err := metricdispatch.LoadData(job, []string{"flops_any", "mem_bw"}, []schema.MetricScope{schema.MetricScopeNode}, ctx, 0, "") if err != nil { cclog.Warnf("Error while loading roofline metrics for job %d", *job.ID) return nil, err diff --git a/internal/metricdispatch/dataLoader.go b/internal/metricdispatch/dataLoader.go index 2970f527..c1b7363b 100644 --- a/internal/metricdispatch/dataLoader.go +++ b/internal/metricdispatch/dataLoader.go @@ -62,9 +62,10 @@ func cacheKey( metrics []string, scopes []schema.MetricScope, resolution int, + resampleAlgo string, ) string { - return fmt.Sprintf("%d(%s):[%v],[%v]-%d", - *job.ID, job.State, metrics, scopes, resolution) + return fmt.Sprintf("%d(%s):[%v],[%v]-%d-%s", + *job.ID, job.State, metrics, scopes, resolution, resampleAlgo) } // LoadData retrieves metric data for a job from the appropriate backend (memory store for running jobs, @@ -87,8 +88,9 @@ func LoadData(job *schema.Job, scopes []schema.MetricScope, ctx context.Context, resolution int, + resampleAlgo string, ) (schema.JobData, error) { - data := cache.Get(cacheKey(job, metrics, scopes, resolution), func() (_ any, ttl time.Duration, size int) { + data := cache.Get(cacheKey(job, metrics, scopes, resolution, resampleAlgo), func() (_ any, ttl time.Duration, size int) { var jd schema.JobData var err error @@ -136,13 +138,17 @@ func LoadData(job *schema.Job, jd = deepCopy(jdTemp) - // Resample archived data using Largest Triangle Three Bucket algorithm to reduce data points - // to the requested resolution, improving transfer performance and client-side rendering. + // Resample archived data to reduce data points to the requested resolution, + // improving transfer performance and client-side rendering. + resampleFn, rfErr := resampler.GetResampler(resampleAlgo) + if rfErr != nil { + return rfErr, 0, 0 + } for _, v := range jd { for _, v_ := range v { timestep := int64(0) for i := 0; i < len(v_.Series); i += 1 { - v_.Series[i].Data, timestep, err = resampler.LargestTriangleThreeBucket(v_.Series[i].Data, int64(v_.Timestep), int64(resolution)) + v_.Series[i].Data, timestep, err = resampleFn(v_.Series[i].Data, int64(v_.Timestep), int64(resolution)) if err != nil { return err, 0, 0 } @@ -414,6 +420,7 @@ func LoadNodeListData( resolution int, from, to time.Time, ctx context.Context, + resampleAlgo string, ) (map[string]schema.JobData, error) { if metrics == nil { for _, m := range archive.GetCluster(cluster).MetricConfig { @@ -428,7 +435,7 @@ func LoadNodeListData( return nil, err } - data, err := ms.LoadNodeListData(cluster, subCluster, nodes, metrics, scopes, resolution, from, to, ctx) + data, err := ms.LoadNodeListData(cluster, subCluster, nodes, metrics, scopes, resolution, from, to, ctx, resampleAlgo) if err != nil { if len(data) != 0 { cclog.Warnf("partial error loading node list data from metric store for cluster %s, subcluster %s: %s", diff --git a/internal/metricdispatch/metricdata.go b/internal/metricdispatch/metricdata.go index 3f03234e..41e702bd 100755 --- a/internal/metricdispatch/metricdata.go +++ b/internal/metricdispatch/metricdata.go @@ -51,7 +51,8 @@ type MetricDataRepository interface { scopes []schema.MetricScope, resolution int, from, to time.Time, - ctx context.Context) (map[string]schema.JobData, error) + ctx context.Context, + resampleAlgo string) (map[string]schema.JobData, error) // HealthCheck evaluates the monitoring state for a set of nodes against expected metrics. HealthCheck(cluster string, diff --git a/internal/metricdispatch/resamplepolicy.go b/internal/metricdispatch/resamplepolicy.go new file mode 100644 index 00000000..14703cbc --- /dev/null +++ b/internal/metricdispatch/resamplepolicy.go @@ -0,0 +1,49 @@ +// Copyright (C) NHR@FAU, University Erlangen-Nuremberg. +// All rights reserved. This file is part of cc-backend. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package metricdispatch + +import "math" + +type ResamplePolicy string + +const ( + ResamplePolicyLow ResamplePolicy = "low" + ResamplePolicyMedium ResamplePolicy = "medium" + ResamplePolicyHigh ResamplePolicy = "high" +) + +// TargetPointsForPolicy returns the target number of data points for a given policy. +func TargetPointsForPolicy(policy ResamplePolicy) int { + switch policy { + case ResamplePolicyLow: + return 200 + case ResamplePolicyMedium: + return 500 + case ResamplePolicyHigh: + return 1000 + default: + return 0 + } +} + +// ComputeResolution computes the resampling resolution in seconds for a given +// job duration, metric frequency, and target point count. Returns 0 if the +// total number of data points is already at or below targetPoints (no resampling needed). +func ComputeResolution(duration int64, frequency int64, targetPoints int) int { + if frequency <= 0 || targetPoints <= 0 || duration <= 0 { + return 0 + } + + totalPoints := duration / frequency + if totalPoints <= int64(targetPoints) { + return 0 + } + + targetRes := math.Ceil(float64(duration) / float64(targetPoints)) + // Round up to nearest multiple of frequency + resolution := int(math.Ceil(targetRes/float64(frequency))) * int(frequency) + + return resolution +} diff --git a/internal/metricdispatch/resamplepolicy_test.go b/internal/metricdispatch/resamplepolicy_test.go new file mode 100644 index 00000000..d192bed2 --- /dev/null +++ b/internal/metricdispatch/resamplepolicy_test.go @@ -0,0 +1,68 @@ +// Copyright (C) NHR@FAU, University Erlangen-Nuremberg. +// All rights reserved. This file is part of cc-backend. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +package metricdispatch + +import "testing" + +func TestTargetPointsForPolicy(t *testing.T) { + tests := []struct { + policy ResamplePolicy + want int + }{ + {ResamplePolicyLow, 200}, + {ResamplePolicyMedium, 500}, + {ResamplePolicyHigh, 1000}, + {ResamplePolicy("unknown"), 0}, + {ResamplePolicy(""), 0}, + } + + for _, tt := range tests { + if got := TargetPointsForPolicy(tt.policy); got != tt.want { + t.Errorf("TargetPointsForPolicy(%q) = %d, want %d", tt.policy, got, tt.want) + } + } +} + +func TestComputeResolution(t *testing.T) { + tests := []struct { + name string + duration int64 + frequency int64 + targetPoints int + want int + }{ + // 24h job, 60s frequency, 1440 total points + {"low_24h_60s", 86400, 60, 200, 480}, + {"medium_24h_60s", 86400, 60, 500, 180}, + {"high_24h_60s", 86400, 60, 1000, 120}, + + // 2h job, 60s frequency, 120 total points — no resampling needed + {"low_2h_60s", 7200, 60, 200, 0}, + {"medium_2h_60s", 7200, 60, 500, 0}, + {"high_2h_60s", 7200, 60, 1000, 0}, + + // Edge: zero/negative inputs + {"zero_duration", 0, 60, 200, 0}, + {"zero_frequency", 86400, 0, 200, 0}, + {"zero_target", 86400, 60, 0, 0}, + {"negative_duration", -100, 60, 200, 0}, + + // 12h job, 30s frequency, 1440 total points + {"medium_12h_30s", 43200, 30, 500, 90}, + + // Exact fit: total points == target points + {"exact_fit", 12000, 60, 200, 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ComputeResolution(tt.duration, tt.frequency, tt.targetPoints) + if got != tt.want { + t.Errorf("ComputeResolution(%d, %d, %d) = %d, want %d", + tt.duration, tt.frequency, tt.targetPoints, got, tt.want) + } + }) + } +} diff --git a/internal/metricstoreclient/cc-metric-store.go b/internal/metricstoreclient/cc-metric-store.go index 55dc7fb5..53ace245 100644 --- a/internal/metricstoreclient/cc-metric-store.go +++ b/internal/metricstoreclient/cc-metric-store.go @@ -617,6 +617,7 @@ func (ccms *CCMetricStore) LoadNodeListData( resolution int, from, to time.Time, ctx context.Context, + resampleAlgo string, ) (map[string]schema.JobData, error) { queries, assignedScope, err := ccms.buildNodeQueries(cluster, subCluster, nodes, metrics, scopes, resolution) if err != nil { diff --git a/internal/repository/testdata/job.db b/internal/repository/testdata/job.db index 729cac96..0817a315 100644 Binary files a/internal/repository/testdata/job.db and b/internal/repository/testdata/job.db differ diff --git a/internal/routerConfig/routes.go b/internal/routerConfig/routes.go index db499397..0b769557 100644 --- a/internal/routerConfig/routes.go +++ b/internal/routerConfig/routes.go @@ -15,6 +15,7 @@ import ( "github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/internal/graph/model" + "github.com/ClusterCockpit/cc-backend/internal/metricdispatch" "github.com/ClusterCockpit/cc-backend/internal/repository" "github.com/ClusterCockpit/cc-backend/web" cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger" @@ -496,13 +497,15 @@ func SetupRoutes(router chi.Router, buildInfo web.Build) { // Get Roles availableRoles, _ := schema.GetValidRolesMap(user) + resampling := resamplingForUser(conf) + page := web.Page{ Title: title, User: *user, Roles: availableRoles, Build: buildInfo, Config: conf, - Resampling: config.Keys.EnableResampling, + Resampling: resampling, Infos: infos, } @@ -589,3 +592,36 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Warning", MsgType: "alert-warning", Message: "Empty search", User: *user, Roles: availableRoles, Build: buildInfo}) } } + +// resamplingForUser returns a ResampleConfig that incorporates the user's +// resample policy preference. If the user has a policy set, it creates a +// policy-derived config with targetPoints and trigger. Otherwise falls back +// to the global config. +func resamplingForUser(conf map[string]any) *config.ResampleConfig { + globalCfg := config.Keys.EnableResampling + + policyStr := "" + if policyVal, ok := conf["plotConfiguration_resamplePolicy"]; ok { + if s, ok := policyVal.(string); ok { + policyStr = s + } + } + + // Fall back to global default policy, then to "medium" + if policyStr == "" && globalCfg != nil { + policyStr = globalCfg.DefaultPolicy + } + if policyStr == "" { + policyStr = "medium" + } + + policy := metricdispatch.ResamplePolicy(policyStr) + targetPoints := metricdispatch.TargetPointsForPolicy(policy) + if targetPoints == 0 { + return globalCfg + } + + return &config.ResampleConfig{ + TargetPoints: targetPoints, + } +} diff --git a/pkg/metricstore/api.go b/pkg/metricstore/api.go index 8e02f44c..d10cfba1 100644 --- a/pkg/metricstore/api.go +++ b/pkg/metricstore/api.go @@ -51,14 +51,15 @@ type APIMetricData struct { // // The request can be customized with flags to include/exclude statistics, raw data, and padding. type APIQueryRequest struct { - Cluster string `json:"cluster"` - Queries []APIQuery `json:"queries"` - ForAllNodes []string `json:"for-all-nodes"` - From int64 `json:"from"` - To int64 `json:"to"` - WithStats bool `json:"with-stats"` - WithData bool `json:"with-data"` - WithPadding bool `json:"with-padding"` + Cluster string `json:"cluster"` + Queries []APIQuery `json:"queries"` + ForAllNodes []string `json:"for-all-nodes"` + From int64 `json:"from"` + To int64 `json:"to"` + WithStats bool `json:"with-stats"` + WithData bool `json:"with-data"` + WithPadding bool `json:"with-padding"` + ResampleAlgo string `json:"resample-algo,omitempty"` } // APIQueryResponse represents the response to an APIQueryRequest. @@ -279,7 +280,7 @@ func FetchData(req APIQueryRequest) (*APIQueryResponse, error) { for _, sel := range sels { data := APIMetricData{} - data.Data, data.From, data.To, data.Resolution, err = ms.Read(sel, query.Metric, req.From, req.To, query.Resolution) + data.Data, data.From, data.To, data.Resolution, err = ms.Read(sel, query.Metric, req.From, req.To, query.Resolution, req.ResampleAlgo) if err != nil { // Skip Error If Just Missing Host or Metric, Continue // Empty Return For Metric Handled Gracefully By Frontend diff --git a/pkg/metricstore/metricstore.go b/pkg/metricstore/metricstore.go index 8a73cde9..1fcd4ab0 100644 --- a/pkg/metricstore/metricstore.go +++ b/pkg/metricstore/metricstore.go @@ -701,7 +701,7 @@ func (m *MemoryStore) WriteToLevel(l *Level, selector []string, ts int64, metric // If the level does not hold the metric itself, the data will be aggregated recursively from the children. // The second and third return value are the actual from/to for the data. Those can be different from // the range asked for if no data was available. -func (m *MemoryStore) Read(selector util.Selector, metric string, from, to, resolution int64) ([]schema.Float, int64, int64, int64, error) { +func (m *MemoryStore) Read(selector util.Selector, metric string, from, to, resolution int64, resampleAlgo string) ([]schema.Float, int64, int64, int64, error) { if from > to { return nil, 0, 0, 0, errors.New("[METRICSTORE]> invalid time range") } @@ -759,7 +759,11 @@ func (m *MemoryStore) Read(selector util.Selector, metric string, from, to, reso } } - data, resolution, err = resampler.LargestTriangleThreeBucket(data, minfo.Frequency, resolution) + resampleFn, rfErr := resampler.GetResampler(resampleAlgo) + if rfErr != nil { + return nil, 0, 0, 0, rfErr + } + data, resolution, err = resampleFn(data, minfo.Frequency, resolution) if err != nil { return nil, 0, 0, 0, err } diff --git a/pkg/metricstore/query.go b/pkg/metricstore/query.go index 8a349b5a..2acc92e2 100644 --- a/pkg/metricstore/query.go +++ b/pkg/metricstore/query.go @@ -621,6 +621,7 @@ func (ccms *InternalMetricStore) LoadNodeListData( resolution int, from, to time.Time, ctx context.Context, + resampleAlgo string, ) (map[string]schema.JobData, error) { // Note: Order of node data is not guaranteed after this point queries, assignedScope, err := buildNodeQueries(cluster, subCluster, nodes, metrics, scopes, int64(resolution)) @@ -636,12 +637,13 @@ func (ccms *InternalMetricStore) LoadNodeListData( } req := APIQueryRequest{ - Cluster: cluster, - Queries: queries, - From: from.Unix(), - To: to.Unix(), - WithStats: true, - WithData: true, + Cluster: cluster, + Queries: queries, + From: from.Unix(), + To: to.Unix(), + WithStats: true, + WithData: true, + ResampleAlgo: resampleAlgo, } resBody, err := FetchData(req) diff --git a/web/frontend/src/Systems.root.svelte b/web/frontend/src/Systems.root.svelte index 4cb85079..b543b20f 100644 --- a/web/frontend/src/Systems.root.svelte +++ b/web/frontend/src/Systems.root.svelte @@ -70,8 +70,6 @@ const ccconfig = $derived(thisInit ? getContext("cc-config") : null); const globalMetrics = $derived(thisInit ? getContext("globalMetrics") : null); const resampleConfig = $derived(thisInit ? getContext("resampling") : null); - const resampleResolutions = $derived(resampleConfig ? [...resampleConfig.resolutions] : []); - const resampleDefault = $derived(resampleConfig ? Math.max(...resampleConfig.resolutions) : 0); const displayNodeOverview = $derived((displayType === 'OVERVIEW')); const systemMetrics = $derived(globalMetrics ? [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))] : []); @@ -85,7 +83,8 @@ return {...pendingUnits}; }); - let selectedResolution = $derived(resampleDefault); + // null lets the backend resolve the resolution from the configured resample policy. + let selectedResolution = $state(null); let to = $derived(presetTo ? presetTo : new Date(Date.now())); let from = $derived(presetFrom ? presetFrom : new Date(nowDate.setHours(nowDate.getHours() - 4))); @@ -159,7 +158,7 @@ - + {#if thisInit} {#if !displayNodeOverview} @@ -176,21 +175,6 @@ - {#if resampleConfig} - - - - Resolution - - {#each resampleResolutions as res} - - {/each} - - - - {/if} {/if} diff --git a/web/frontend/src/config/AdminSettings.svelte b/web/frontend/src/config/AdminSettings.svelte index 5d61e66c..5c777c3a 100644 --- a/web/frontend/src/config/AdminSettings.svelte +++ b/web/frontend/src/config/AdminSettings.svelte @@ -16,6 +16,7 @@ import Options from "./admin/Options.svelte"; import NoticeEdit from "./admin/NoticeEdit.svelte"; import RunTaggers from "./admin/RunTaggers.svelte"; + import PlotRenderOptions from "./user/PlotRenderOptions.svelte"; /* Svelte 5 Props */ let { @@ -29,6 +30,8 @@ /* State Init */ let users = $state([]); let roles = $state([]); + let message = $state({ msg: "", target: "", color: "#d63384" }); + let displayMessage = $state(false); /* Functions */ function getUserList() { @@ -52,6 +55,37 @@ getValidRoles(); } + async function handleSettingSubmit(event, setting) { + event.preventDefault(); + + const selector = setting.selector + const target = setting.target + let form = document.querySelector(selector); + let formData = new FormData(form); + try { + const res = await fetch(form.action, { method: "POST", body: formData }); + if (res.ok) { + let text = await res.text(); + popMessage(text, target, "#048109"); + } else { + let text = await res.text(); + throw new Error("Response Code " + res.status + "-> " + text); + } + } catch (err) { + popMessage(err, target, "#d63384"); + } + + return false; + } + + function popMessage(response, restarget, rescolor) { + message = { msg: response, target: restarget, color: rescolor }; + displayMessage = true; + setTimeout(function () { + displayMessage = false; + }, 3500); + } + /* on Mount */ onMount(() => initAdmin()); @@ -73,3 +107,4 @@ + handleSettingSubmit(e, newSetting)}/> diff --git a/web/frontend/src/config/admin/Options.svelte b/web/frontend/src/config/admin/Options.svelte index ea6809f3..50e1fcf4 100644 --- a/web/frontend/src/config/admin/Options.svelte +++ b/web/frontend/src/config/admin/Options.svelte @@ -77,8 +77,8 @@ Metric Plot Resampling Info -

Triggered at {resampleConfig.trigger} datapoints.

-

Configured resolutions: {resampleConfig.resolutions}

+

Resampling is enabled.

+

Target data points per plot: {resampleConfig.targetPoints}

diff --git a/web/frontend/src/config/user/PlotRenderOptions.svelte b/web/frontend/src/config/user/PlotRenderOptions.svelte index de585853..c26c93f3 100644 --- a/web/frontend/src/config/user/PlotRenderOptions.svelte +++ b/web/frontend/src/config/user/PlotRenderOptions.svelte @@ -16,6 +16,7 @@ Card, CardTitle, } from "@sveltestrap/sveltestrap"; + import { getContext } from "svelte"; import { fade } from "svelte/transition"; /* Svelte 5 Props */ @@ -25,6 +26,8 @@ displayMessage = $bindable(), updateSetting } = $props(); + + const resampleConfig = getContext("resampling"); @@ -64,7 +67,7 @@ id="lwvalue" name="value" aria-describedby="lineWidthHelp" - value={config.plotConfiguration_lineWidth} + value={config?.plotConfiguration_lineWidth} min="1" />
@@ -111,7 +114,7 @@ id="pprvalue" name="value" aria-describedby="plotsperrowHelp" - value={config.plotConfiguration_plotsPerRow} + value={config?.plotConfiguration_plotsPerRow} min="1" />
@@ -153,7 +156,7 @@
- {#if config.plotConfiguration_colorBackground} + {#if config?.plotConfiguration_colorBackground} {:else} @@ -161,7 +164,7 @@
- {#if config.plotConfiguration_colorBackground} + {#if config?.plotConfiguration_colorBackground} {:else} @@ -219,4 +222,90 @@ + + + + +
updateSetting(e, { + selector: "#resample-policy-form", + target: "rsp", + })} + > + +
Resample Policy
+ {#if displayMessage && message.target == "rsp"} +
+ + Update: {message.msg} + +
+ {/if} +
+ +
+ {#each [["", "Default"], ["low", "Low"], ["medium", "Medium"], ["high", "High"]] as [val, label]} +
+ + +
+ {/each} +
+ Controls how many data points are shown in metric plots. Low = fast overview (~200 points), Medium = balanced (~500), High = maximum detail (~1000). +
+
+ +
+
+ + + + + +
updateSetting(e, { + selector: "#resample-algo-form", + target: "rsa", + })} + > + +
Resample Algorithm
+ {#if displayMessage && message.target == "rsa"} +
+ + Update: {message.msg} + +
+ {/if} +
+ +
+ {#each [["", "Default"], ["lttb", "LTTB"], ["average", "Average"], ["simple", "Simple"]] as [val, label]} +
+ + +
+ {/each} +
+ Algorithm used when downsampling time-series data. LTTB preserves visual shape, Average smooths data, Simple picks every Nth point. +
+
+ +
+
+ \ No newline at end of file diff --git a/web/frontend/src/generic/joblist/JobListRow.svelte b/web/frontend/src/generic/joblist/JobListRow.svelte index c0b18e63..43c4d22d 100644 --- a/web/frontend/src/generic/joblist/JobListRow.svelte +++ b/web/frontend/src/generic/joblist/JobListRow.svelte @@ -84,7 +84,6 @@ let thresholdStates = $state({}); /* Derived */ - const resampleDefault = $derived(resampleConfig ? Math.max(...resampleConfig.resolutions) : 0); const jobId = $derived(job.id); const scopes = $derived.by(() => { if (job.numNodes == 1) { @@ -95,7 +94,9 @@ }; }); - let selectedResolution = $derived(resampleDefault); + // null lets the backend resolve the resolution from the configured resample + // policy; zoom interactions override it with an explicit value. + let selectedResolution = $state(null); let isSelected = $derived(previousSelect); let metricsQuery = $derived(queryStore({ client: client, diff --git a/web/frontend/src/generic/plots/MetricPlot.svelte b/web/frontend/src/generic/plots/MetricPlot.svelte index 7f72a298..e937f32f 100644 --- a/web/frontend/src/generic/plots/MetricPlot.svelte +++ b/web/frontend/src/generic/plots/MetricPlot.svelte @@ -73,9 +73,10 @@ const subClusterTopology = $derived(getContext("getHardwareTopology")(cluster, subCluster)); const metricConfig = $derived(getContext("getMetricConfig")(cluster, subCluster, metric)); const usesMeanStatsSeries = $derived((statisticsSeries?.mean && statisticsSeries.mean.length != 0)); - const resampleTrigger = $derived(resampleConfig?.trigger ? Number(resampleConfig.trigger) : null); + const resampleTrigger = $derived(resampleConfig?.trigger ? Number(resampleConfig.trigger) : (resampleConfig?.targetPoints ? Math.floor(resampleConfig.targetPoints / 4) : null)); const resampleResolutions = $derived(resampleConfig?.resolutions ? [...resampleConfig.resolutions] : null); const resampleMinimum = $derived(resampleConfig?.resolutions ? Math.min(...resampleConfig.resolutions) : null); + const resampleTargetPoints = $derived(resampleConfig?.targetPoints ? Number(resampleConfig.targetPoints) : null); const useStatsSeries = $derived(!!statisticsSeries); // Display Stats Series By Default if Exists const thresholds = $derived(findJobAggregationThresholds( subClusterTopology, @@ -515,24 +516,29 @@ if (resampleConfig && !forNode && key === 'x') { const numX = (u.series[0].idxs[1] - u.series[0].idxs[0]) if (numX <= resampleTrigger && timestep !== resampleMinimum) { - /* Get closest zoom level; prevents multiple iterative zoom requests for big zoom-steps (e.g. 600 -> 300 -> 120 -> 60) */ - // Which resolution to theoretically request to achieve 30 or more visible data points: - const target = (numX * timestep) / resampleTrigger - // Which configured resolution actually matches the closest to theoretical target: - const closest = resampleResolutions.reduce(function(prev, curr) { - return (Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev); - }); + let newRes; + if (resampleTargetPoints && !resampleResolutions) { + // Policy-based: compute resolution dynamically from visible window + const visibleDuration = (u.scales.x.max - u.scales.x.min); + const nativeTimestep = metricConfig?.timestep || timestep; + newRes = Math.ceil(visibleDuration / resampleTargetPoints / nativeTimestep) * nativeTimestep; + if (newRes < nativeTimestep) newRes = nativeTimestep; + } else if (resampleResolutions) { + // Array-based: find closest configured resolution + const target = (numX * timestep) / resampleTrigger; + newRes = resampleResolutions.reduce(function(prev, curr) { + return (Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev); + }); + } // Prevents non-required dispatches - if (timestep !== closest) { - // console.log('Dispatch: Zoom with Res from / to', timestep, closest) + if (newRes && timestep !== newRes) { onZoom({ - newRes: closest, + newRes: newRes, lastZoomState: u?.scales, lastThreshold: thresholds?.normal }); } } else { - // console.log('Dispatch: Zoom Update States') onZoom({ lastZoomState: u?.scales, lastThreshold: thresholds?.normal diff --git a/web/frontend/src/job/Metric.svelte b/web/frontend/src/job/Metric.svelte index a0ab18e5..192d8f22 100644 --- a/web/frontend/src/job/Metric.svelte +++ b/web/frontend/src/job/Metric.svelte @@ -44,7 +44,6 @@ const client = getContextClient(); const statsPattern = /(.*)-stat$/; const resampleConfig = getContext("resampling") || null; - const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0; const subQuery = gql` query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!, $selectedResolution: Int) { singleUpdate: jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes, resolution: $selectedResolution) { @@ -78,7 +77,9 @@ `; /* State Init */ - let selectedResolution = $state(resampleDefault); + // null lets the backend resolve the resolution from the configured resample + // policy; zoom interactions override it with an explicit value. + let selectedResolution = $state(null); let selectedHost = $state(null); let zoomState = $state(null); let thresholdState = $state(null); diff --git a/web/web.go b/web/web.go index 7f3c32e9..685256f9 100644 --- a/web/web.go +++ b/web/web.go @@ -72,6 +72,8 @@ type PlotConfiguration struct { PlotsPerRow int `json:"plots-per-row"` LineWidth int `json:"line-width"` ColorScheme []string `json:"color-scheme"` + ResampleAlgo string `json:"resample-algo"` + ResamplePolicy string `json:"resample-policy"` } const ( @@ -170,6 +172,8 @@ func Init(rawConfig json.RawMessage) error { UIDefaultsMap["plotConfiguration_plotsPerRow"] = UIDefaults.PlotConfiguration.PlotsPerRow UIDefaultsMap["plotConfiguration_lineWidth"] = UIDefaults.PlotConfiguration.LineWidth UIDefaultsMap["plotConfiguration_colorScheme"] = UIDefaults.PlotConfiguration.ColorScheme + UIDefaultsMap["plotConfiguration_resampleAlgo"] = UIDefaults.PlotConfiguration.ResampleAlgo + UIDefaultsMap["plotConfiguration_resamplePolicy"] = UIDefaults.PlotConfiguration.ResamplePolicy for _, c := range UIDefaults.MetricConfig.Clusters { if c.JobListMetrics != nil {