From c0d2d65f9636fe229ab1ca9a023d6b8076352dd4 Mon Sep 17 00:00:00 2001 From: Jan Eitzinger Date: Thu, 19 Mar 2026 21:16:48 +0100 Subject: [PATCH] Introduce average resampler support Fixes #526 Entire-Checkpoint: 893a1de325b5 --- api/schema.graphqls | 8 +++ go.mod | 50 +++++++-------- go.sum | 51 ++++++++++++++-- internal/api/api_test.go | 2 +- internal/api/job.go | 6 +- internal/archiver/archiver.go | 2 +- internal/graph/generated/generated.go | 50 ++++++++++++--- internal/graph/model/models_gen.go | 57 ++++++++++++++++++ internal/graph/schema.resolvers.go | 18 ++++-- internal/graph/util.go | 2 +- internal/metricdispatch/dataLoader.go | 21 ++++--- internal/metricdispatch/metricdata.go | 3 +- internal/metricstoreclient/cc-metric-store.go | 1 + internal/repository/testdata/job.db | Bin 987136 -> 1146880 bytes pkg/metricstore/api.go | 19 +++--- pkg/metricstore/metricstore.go | 8 ++- pkg/metricstore/query.go | 14 +++-- 17 files changed, 239 insertions(+), 73 deletions(-) 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/go.mod b/go.mod index 1e3b7bf1..f830a366 100644 --- a/go.mod +++ b/go.mod @@ -7,28 +7,30 @@ tool ( github.com/swaggo/swag/cmd/swag ) +replace github.com/ClusterCockpit/cc-lib/v2 => ../../prg/CC/cc-lib + require ( github.com/99designs/gqlgen v0.17.88 - github.com/ClusterCockpit/cc-lib/v2 v2.9.1 + github.com/ClusterCockpit/cc-lib/v2 v2.10.0 github.com/ClusterCockpit/cc-line-protocol/v2 v2.4.0 github.com/Masterminds/squirrel v1.5.4 - github.com/aws/aws-sdk-go-v2 v1.41.3 - github.com/aws/aws-sdk-go-v2/config v1.32.11 - github.com/aws/aws-sdk-go-v2/credentials v1.19.11 - github.com/aws/aws-sdk-go-v2/service/s3 v1.97.0 + github.com/aws/aws-sdk-go-v2 v1.41.4 + github.com/aws/aws-sdk-go-v2/config v1.32.12 + github.com/aws/aws-sdk-go-v2/credentials v1.19.12 + github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 github.com/coreos/go-oidc/v3 v3.17.0 github.com/expr-lang/expr v1.17.8 github.com/go-chi/chi/v5 v5.2.5 github.com/go-chi/cors v1.2.2 github.com/go-co-op/gocron/v2 v2.19.1 - github.com/go-ldap/ldap/v3 v3.4.12 + github.com/go-ldap/ldap/v3 v3.4.13 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang-migrate/migrate/v4 v4.19.1 github.com/google/gops v0.3.29 github.com/gorilla/sessions v1.4.0 github.com/jmoiron/sqlx v1.4.0 github.com/joho/godotenv v1.5.1 - github.com/mattn/go-sqlite3 v1.14.34 + github.com/mattn/go-sqlite3 v1.14.37 github.com/parquet-go/parquet-go v0.29.0 github.com/qustavo/sqlhooks/v2 v2.1.0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 @@ -48,20 +50,20 @@ require ( github.com/agnivade/levenshtein v1.2.1 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect github.com/aws/smithy-go v1.24.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -94,9 +96,9 @@ require ( github.com/nats-io/nats.go v1.49.0 // indirect github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nuid v1.0.1 // indirect - github.com/oapi-codegen/runtime v1.2.0 // indirect + github.com/oapi-codegen/runtime v1.3.0 // indirect github.com/parquet-go/bitpack v1.0.0 // indirect - github.com/parquet-go/jsonlite v1.4.0 // indirect + github.com/parquet-go/jsonlite v1.5.0 // indirect github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect @@ -110,7 +112,7 @@ require ( github.com/urfave/cli/v2 v2.27.7 // indirect github.com/urfave/cli/v3 v3.7.0 // indirect github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/mod v0.34.0 // indirect golang.org/x/net v0.52.0 // indirect diff --git a/go.sum b/go.sum index 4006036e..e8f9704c 100644 --- a/go.sum +++ b/go.sum @@ -4,12 +4,6 @@ github.com/99designs/gqlgen v0.17.88 h1:neMQDgehMwT1vYIOx/w5ZYPUU/iMNAJzRO44I5In github.com/99designs/gqlgen v0.17.88/go.mod h1:qeqYFEgOeSKqWedOjogPizimp2iu4E23bdPvl4jTYic= github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A= github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= -github.com/ClusterCockpit/cc-lib/v2 v2.8.2 h1:rCLZk8wz8yq8xBnBEdVKigvA2ngR8dPmHbEFwxxb3jw= -github.com/ClusterCockpit/cc-lib/v2 v2.8.2/go.mod h1:FwD8vnTIbBM3ngeLNKmCvp9FoSjQZm7xnuaVxEKR23o= -github.com/ClusterCockpit/cc-lib/v2 v2.9.0 h1:mzUYakcjwb+UP5II4jOvr36rSYct90gXBbtUg+nvm9c= -github.com/ClusterCockpit/cc-lib/v2 v2.9.0/go.mod h1:FwD8vnTIbBM3ngeLNKmCvp9FoSjQZm7xnuaVxEKR23o= -github.com/ClusterCockpit/cc-lib/v2 v2.9.1 h1:eplKhXQyGAElBGCEGdmxwj7fLv26Op16uK0KxUePDak= -github.com/ClusterCockpit/cc-lib/v2 v2.9.1/go.mod h1:FwD8vnTIbBM3ngeLNKmCvp9FoSjQZm7xnuaVxEKR23o= github.com/ClusterCockpit/cc-line-protocol/v2 v2.4.0 h1:hIzxgTBWcmCIHtoDKDkSCsKCOCOwUC34sFsbD2wcW0Q= github.com/ClusterCockpit/cc-line-protocol/v2 v2.4.0/go.mod h1:y42qUu+YFmu5fdNuUAS4VbbIKxVjxCvbVqFdpdh8ahY= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= @@ -45,40 +39,76 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA= github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= +github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 h1:3kGOqnh1pPeddVa/E37XNTaWJ8W6vrbYV9lJEkCnhuY= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs= github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo= +github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= +github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc= github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 h1:qi3e/dmpdONhj1RyIZdi6DKKpDXS5Lb8ftr3p7cyHJc= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 h1:SwGMTMLIlvDNyhMteQ6r8IJSBPlRdXX5d4idhIGbkXA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 h1:BYf7XNsJMzl4mObARUBUib+j2tf0U//JAAtTnYqvqCw= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11/go.mod h1:aEUS4WrNk/+FxkBZZa7tVgp4pGH+kFGW40Y8rCPqt5g= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 h1:qtJZ70afD3ISKWnoX3xB0J2otEqu3LqicRcDBqsj0hQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8zjTHSXFhB9L/2OY8Dqs0xXiLjF30jA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw= github.com/aws/aws-sdk-go-v2/service/s3 v1.97.0 h1:zyKY4OxzUImu+DigelJI9o49QQv8CjREs5E1CywjtIA= github.com/aws/aws-sdk-go-v2/service/s3 v1.97.0/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 h1:csi9NLpFZXb9fxY7rS1xVzgPRGMt7MSNWeQ6eo247kE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0= github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row= github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias= github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo= github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -115,6 +145,8 @@ github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZR github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4= github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo= +github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ= +github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0= github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= @@ -219,6 +251,8 @@ github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsO github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= +github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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= @@ -235,11 +269,15 @@ 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/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/oapi-codegen/runtime v1.3.0 h1:vyK1zc0gDWWXgk2xoQa4+X4RNNc5SL2RbTpJS/4vMYA= +github.com/oapi-codegen/runtime v1.3.0/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/parquet-go/bitpack v1.0.0 h1:AUqzlKzPPXf2bCdjfj4sTeacrUwsT7NlcYDMUQxPcQA= github.com/parquet-go/bitpack v1.0.0/go.mod h1:XnVk9TH+O40eOOmvpAVZ7K2ocQFrQwysLMnc6M/8lgs= github.com/parquet-go/jsonlite v1.4.0 h1:RTG7prqfO0HD5egejU8MUDBN8oToMj55cgSV1I0zNW4= github.com/parquet-go/jsonlite v1.4.0/go.mod h1:nDjpkpL4EOtqs6NQugUsi0Rleq9sW/OtC1NnZEnxzF0= +github.com/parquet-go/jsonlite v1.5.0 h1:ulS7lNWdPwiqDMLzTiXHYmIUhu99mavZh2iAVdXet3g= +github.com/parquet-go/jsonlite v1.5.0/go.mod h1:nDjpkpL4EOtqs6NQugUsi0Rleq9sW/OtC1NnZEnxzF0= github.com/parquet-go/parquet-go v0.29.0 h1:xXlPtFVR51jpSVzf+cgHnNIcb7Xet+iuvkbe0HIm90Y= github.com/parquet-go/parquet-go v0.29.0/go.mod h1:navtkAYr2LGoJVp141oXPlO/sxLvaOe3la2JEoD8+rg= github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= @@ -305,6 +343,7 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +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= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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 76ec3e2a..ec76549d 100644 --- a/internal/api/job.go +++ b/internal/api/job.go @@ -301,7 +301,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 @@ -397,7 +397,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 @@ -1078,7 +1078,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/graph/generated/generated.go b/internal/graph/generated/generated.go index a5319fc7..c8354b8a 100644 --- a/internal/graph/generated/generated.go +++ b/internal/graph/generated/generated.go @@ -326,7 +326,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 @@ -334,7 +334,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 @@ -482,7 +482,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) @@ -491,7 +491,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 { @@ -1665,7 +1665,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 @@ -1753,7 +1753,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 @@ -2524,6 +2524,12 @@ type TimeWeights { coreHours: [NullableFloat!]! } +enum ResampleAlgo { + LTTB + AVERAGE + SIMPLE +} + enum Aggregate { USER PROJECT @@ -2614,6 +2620,7 @@ type Query { metrics: [String!] scopes: [MetricScope!] resolution: Int + resampleAlgo: ResampleAlgo ): [JobMetricWithName!]! jobStats(id: ID!, metrics: [String!]): [NamedStats!]! @@ -2673,6 +2680,7 @@ type Query { to: Time! page: PageRequest resolution: Int + resampleAlgo: ResampleAlgo ): NodesResultList! clusterMetrics( @@ -3006,6 +3014,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 } @@ -3183,6 +3196,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 } @@ -9436,7 +9454,7 @@ func (ec *executionContext) _Query_jobMetrics(ctx context.Context, field graphql ec.fieldContext_Query_jobMetrics, 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, ec.marshalNJobMetricWithName2ᚕᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐJobMetricWithNameᚄ, @@ -9917,7 +9935,7 @@ func (ec *executionContext) _Query_nodeMetricsList(ctx context.Context, field gr ec.fieldContext_Query_nodeMetricsList, 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, ec.marshalNNodesResultList2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐNodesResultList, @@ -19672,6 +19690,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/schema.resolvers.go b/internal/graph/schema.resolvers.go index c84cb713..b0f311d1 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -498,7 +498,7 @@ 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) { +func (r *queryResolver) JobMetrics(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope, resolution *int, resampleAlgo *model.ResampleAlgo) ([]*model.JobMetricWithName, error) { if resolution == nil { // Load from Config if config.Keys.EnableResampling != nil { defaultRes := slices.Max(config.Keys.EnableResampling.Resolutions) @@ -515,7 +515,12 @@ func (r *queryResolver) JobMetrics(ctx context.Context, id string, metrics []str return nil, err } - data, err := metricdispatch.LoadData(job, metrics, scopes, ctx, *resolution) + algoName := "" + if resampleAlgo != nil { + algoName = strings.ToLower(resampleAlgo.String()) + } + + data, err := metricdispatch.LoadData(job, metrics, scopes, ctx, *resolution, algoName) if err != nil { cclog.Warn("Error while loading job data") return nil, err @@ -872,7 +877,7 @@ 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) { +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) { if resolution == nil { // Load from Config if config.Keys.EnableResampling != nil { defaultRes := slices.Max(config.Keys.EnableResampling.Resolutions) @@ -901,8 +906,13 @@ func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, sub } } + algoName := "" + if resampleAlgo != nil { + algoName = strings.ToLower(resampleAlgo.String()) + } + // 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/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 729cac965265a81bf187475d23d21de268a42ccd..0817a315801162acf6511a2aaa6269d1819f817e 100644 GIT binary patch literal 1146880 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