[release-12.0.5] Backport aws-sdk-go-v2 update and subsequent fixes (#111243)

* CloudWatch: Backport aws-sdk-go-v2 update from external plugin (#107136)

(cherry picked from a18ea34688)

* Datasources: Update grafana-aws-sdk for new sigv4 middleware and aws-sdk-go v1 removal (#107522)

(cherry picked from commit 66d9a33cc9)

* CloudWatch: Fix proxy transport issue (#107807)

(cherry picked from c3eeb1fcd9)

* CloudWatch: Fix http client handling + assume role bug (#107893)

(cherry picked from commit f34a9fc0c2)

* CloudWatch: Use default region when query region is unset (#109089)

(cherry picked from commit 5f4097a159)

* CloudWatch: Fix handling region for legacy alerts (#109217)

(cherry picked from commit 2bf9aea8ef)

* Update go.mod owners

---------

Co-authored-by: Isabella Siu <Isabella.siu@grafana.com>
This commit is contained in:
Nathan Vērzemnieks 2025-09-19 03:46:40 -07:00 committed by GitHub
parent 6b87718cde
commit 560be84dc8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
105 changed files with 2706 additions and 3129 deletions

38
go.mod
View File

@ -89,7 +89,7 @@ require (
github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend
github.com/grafana/grafana-app-sdk v0.35.1 // @grafana/grafana-app-platform-squad
github.com/grafana/grafana-app-sdk/logging v0.35.1 // @grafana/grafana-app-platform-squad
github.com/grafana/grafana-aws-sdk v0.31.5 // @grafana/aws-datasources
github.com/grafana/grafana-aws-sdk v0.31.6-0.20250918141554-0b96cca5e46b // @grafana/aws-datasources
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // @grafana/partner-datasources
github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 // @grafana/grafana-operator-experience-squad
github.com/grafana/grafana-google-sdk-go v0.2.1 // @grafana/partner-datasources
@ -276,25 +276,25 @@ require (
github.com/armon/go-metrics v0.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/at-wat/mqtt-go v0.19.4 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
github.com/aws/aws-sdk-go-v2 v1.39.0 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.8 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
github.com/aws/smithy-go v1.20.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 // indirect
github.com/aws/smithy-go v1.23.0 // @grafana/aws-datasources
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
@ -581,6 +581,14 @@ require (
require github.com/urfave/cli/v3 v3.3.8 // @grafana/grafana-backend-group
require (
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.50.1 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.58.0 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/ec2 v1.252.0 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/oam v1.22.3 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.30.4 // @grafana/aws-datasources
)
require github.com/cenkalti/backoff/v5 v5.0.2 // indirect
// Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream

70
go.sum
View File

@ -843,44 +843,54 @@ github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2z
github.com/aws/aws-sdk-go v1.50.29/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
github.com/aws/aws-sdk-go-v2 v1.39.0 h1:xm5WV/2L4emMRmMjHFykqiA4M/ra0DJVSWUkDyBjbg4=
github.com/aws/aws-sdk-go-v2 v1.39.0/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
github.com/aws/aws-sdk-go-v2/config v1.31.8 h1:kQjtOLlTU4m4A64TsRcqwNChhGCwaPBt+zCQt/oWsHU=
github.com/aws/aws-sdk-go-v2/config v1.31.8/go.mod h1:QPpc7IgljrKwH0+E6/KolCgr4WPLerURiU592AYzfSY=
github.com/aws/aws-sdk-go-v2/credentials v1.18.12 h1:zmc9e1q90wMn8wQbjryy8IwA6Q4XlaL9Bx2zIqdNNbk=
github.com/aws/aws-sdk-go-v2/credentials v1.18.12/go.mod h1:3VzdRDR5u3sSJRI4kYcOSIBbeYsgtVk7dG5R/U6qLWY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 h1:Is2tPmieqGS2edBnmOJIbdvOA6Op+rRpaYR60iBAwXM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7/go.mod h1:F1i5V5421EGci570yABvpIXgRIBPb5JM+lSkHF6Dq5w=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 h1:zeN9UtUlA6FTx0vFSayxSX32HDw73Yb6Hh2izDSFxXY=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10/go.mod h1:3HKuexPDcwLWPaqpW2UR/9n8N/u/3CKcGAzSs8p8u8g=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 h1:UCxq0X9O3xrlENdKf1r9eRJoKz/b0AfGkpp3a7FPlhg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7/go.mod h1:rHRoJUNUASj5Z/0eqI4w32vKvC7atoWR0jC+IkmVH8k=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 h1:Y6DTZUn7ZUC4th9FMBbo8LVE+1fyq3ofw+tRwkUd3PY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7/go.mod h1:x3XE6vMnU9QvHN/Wrx2s44kwzV2o2g5x/siw4ZUJ9g8=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.50.1 h1:OSye2F+X+KfxEdbrOT3x+p7L3kr5zPtm3BMkNWGVXQ8=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.50.1/go.mod h1:bNNaZaAX81KIuYDaj5ODgZwA1ybBJzpDeKYoNxEGGqw=
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.58.0 h1:XH0kj0KcoKd+BAadpiS83/Wf+25q4FmH3gDei4u+PzA=
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.58.0/go.mod h1:ptJgRWK9opQK1foOTBKUg3PokkKA0/xcTXWIxwliaIY=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.252.0 h1:4C/MhyZWR0Bk+7QLVvNxa4xhwEnqm1KclA5MCMepjF0=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.252.0/go.mod h1:MXJiLJZtMqb2dVXgEIn35d5+7MqLd4r8noLen881kpk=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 h1:mLgc5QIgOy26qyh5bvW+nDoAppxgn3J2WV3m9ewq7+8=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7/go.mod h1:wXb/eQnqt8mDQIQTTmcw58B5mYGxzLGZGK8PWNFZ0BA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg=
github.com/aws/aws-sdk-go-v2/service/oam v1.22.3 h1:DCKlfaaVB7mE7dtfGD60gWX1Y5nc+yh8qcBHvpdkRqs=
github.com/aws/aws-sdk-go-v2/service/oam v1.22.3/go.mod h1:uSLwrlkn0YO7P4xzMy4yJDgyyi6BYzZA73D0iv5gPpo=
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.30.4 h1:LmoqYCi723i8jvkALGA7E+1GeaOc2OHZNLdkwp7cjZA=
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.30.4/go.mod h1:KV1rGdzLiPDfq5EId56EPFzKL5f3FQ8vB4kN/RkkVC4=
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 h1:hT8ZAZRIfqBqHbzKTII+CIiY8G2oC9OpLedkZ51DWl8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 h1:7PKX3VYsZ8LUWceVRuv0+PU+E7OtQb1lgmi5vmUE9CM=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3/go.mod h1:Ql6jE9kyyWI5JHn+61UT/Y5Z0oyVJGmgmJbZD5g4unY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 h1:e0XBRn3AptQotkyBFrHAxFB8mDhAIOfsG+7KyJ0dg98=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4/go.mod h1:XclEty74bsGBCr1s0VSaA11hQ4ZidK4viWK7rRfO88I=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 h1:PR00NXRYgY4FWHqOGx3fC3lhVKjsp1GdloDv2ynMSd8=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4/go.mod h1:Z+Gd23v97pX9zK97+tX4ppAgqCt3Z2dIXB02CtBncK8=
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f/go.mod h1:2stgcRjl6QmW+gU2h5E7BQXg4HU0gzxKWDuT5HviN9s=
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 h1:60m4tnanN1ctzIu4V3bfCNJ39BiOPSm1gHFlFjTkRE0=
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c=
@ -1595,8 +1605,8 @@ github.com/grafana/grafana-app-sdk v0.35.1 h1:zEXubzsQrxGBOzXJJMBwhEClC/tvPi0sfK
github.com/grafana/grafana-app-sdk v0.35.1/go.mod h1:Zx5MkVppYK+ElSDUAR6+fjzOVo6I/cIgk+ty+LmNOxI=
github.com/grafana/grafana-app-sdk/logging v0.35.1 h1:taVpl+RoixTYl0JBJGhH+fPVmwA9wvdwdzJTZsv9buM=
github.com/grafana/grafana-app-sdk/logging v0.35.1/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk=
github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX/Gh3FZKBE=
github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM=
github.com/grafana/grafana-aws-sdk v0.31.6-0.20250918141554-0b96cca5e46b h1:25XvbNSo8Sgi3MBFOtToRvlbXwfQ44nTb/NYCpZxUJE=
github.com/grafana/grafana-aws-sdk v0.31.6-0.20250918141554-0b96cca5e46b/go.mod h1:2JJbVLc3jjWu0aAQx4bhPa2ChOUjLs9x7ime6mB4O+o=
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 h1:OfCkitCuomzZKW1WYHrG8MxKwtMhALb7jqoj+487eTg=
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo=
github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 h1:S4kHwr//AqhtL9xHBtz1gqVgZQeCRGTxjgsRBAkpjKY=

View File

@ -731,7 +731,12 @@ github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
github.com/aws/aws-sdk-go-v2 v1.36.0/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM=
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 h1:UPTdlTOwWUX49fVi7cymEN6hDqCwe3LNv1vi7TXUutk=
github.com/aws/aws-sdk-go-v2/service/kms v1.35.3/go.mod h1:gjDP16zn+WWalyaUqwCCioQ8gU8lzttCCc9jYsiQI/8=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.4 h1:NgRFYyFpiMD62y4VPXh4DosPFbZd4vdMVBWKk0VmWXc=
@ -743,6 +748,7 @@ github.com/aws/aws-sdk-go-v2/service/sqs v1.34.3/go.mod h1:L0enV3GCRd5iG9B64W35C
github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4 h1:hgSBvRT7JEWx2+vEGI9/Ld5rZtl7M5lu8PqdvOmbRHw=
github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4/go.mod h1:v7NIzEFIHBiicOMaMTuEmbnzGnqW0d+6ulNALul6fYE=
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
@ -1238,6 +1244,7 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0/go.mod h1:zrT2dxOAjNFPRGjTUe2Xmb4q4YdUwVvQFV6xiCSf+z0=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=

View File

@ -49,7 +49,7 @@ func TestCallResource(t *testing.T) {
cfg.StaticRootPath = staticRootPath
cfg.Azure = &azsettings.AzureSettings{}
coreRegistry := coreplugin.ProvideCoreRegistry(tracing.InitializeTracerForTest(), nil, &cloudwatch.CloudWatchService{}, nil, nil, nil, nil,
coreRegistry := coreplugin.ProvideCoreRegistry(tracing.InitializeTracerForTest(), nil, &cloudwatch.Service{}, nil, nil, nil, nil,
nil, nil, nil, nil, testdatasource.ProvideService(), nil, nil, nil, nil, nil, nil, nil, nil)
testCtx := pluginsintegration.CreateIntegrationTestCtx(t, cfg, coreRegistry)

View File

@ -4,9 +4,7 @@ import (
"net/http"
"time"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
awssdk "github.com/grafana/grafana-aws-sdk/pkg/sigv4"
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
"github.com/grafana/grafana-aws-sdk/pkg/awsauth"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/mwitkow/go-conntrack"
@ -46,21 +44,7 @@ func New(cfg *setting.Cfg, validator validations.DataSourceRequestURLValidator,
// SigV4 signing should be performed after all headers are added
if cfg.SigV4AuthEnabled {
authSettings := awsds.AuthSettings{
AllowedAuthProviders: cfg.AWSAllowedAuthProviders,
AssumeRoleEnabled: cfg.AWSAssumeRoleEnabled,
ExternalID: cfg.AWSExternalId,
ListMetricsPageLimit: cfg.AWSListMetricsPageLimit,
SecureSocksDSProxyEnabled: cfg.SecureSocksDSProxy.Enabled,
}
if cfg.AWSSessionDuration != "" {
sessionDuration, err := gtime.ParseDuration(cfg.AWSSessionDuration)
if err == nil {
authSettings.SessionDuration = &sessionDuration
}
}
middlewares = append(middlewares, awssdk.SigV4MiddlewareWithAuthSettings(cfg.SigV4VerboseLogging, authSettings))
middlewares = append(middlewares, awsauth.NewSigV4Middleware())
}
setDefaultTimeoutOptions(cfg)

View File

@ -5,7 +5,7 @@ import (
"github.com/grafana/grafana/pkg/services/validations"
awssdk "github.com/grafana/grafana-aws-sdk/pkg/sigv4"
"github.com/grafana/grafana-aws-sdk/pkg/awsauth"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/setting"
@ -63,7 +63,7 @@ func TestHTTPClientProvider(t *testing.T) {
require.Equal(t, sdkhttpclient.ResponseLimitMiddlewareName, o.Middlewares[6].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, HostRedirectValidationMiddlewareName, o.Middlewares[7].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, sdkhttpclient.ErrorSourceMiddlewareName, o.Middlewares[8].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, awssdk.SigV4MiddlewareName, o.Middlewares[9].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, awsauth.NewSigV4Middleware().(sdkhttpclient.MiddlewareName).MiddlewareName(), o.Middlewares[9].(sdkhttpclient.MiddlewareName).MiddlewareName())
})
t.Run("When creating new provider and http logging is enabled for one plugin, it should apply expected middleware", func(t *testing.T) {

View File

@ -94,7 +94,7 @@ func NewRegistry(store map[string]backendplugin.PluginFactoryFunc) *Registry {
}
}
func ProvideCoreRegistry(tracer tracing.Tracer, am *azuremonitor.Service, cw *cloudwatch.CloudWatchService, cm *cloudmonitoring.Service,
func ProvideCoreRegistry(tracer tracing.Tracer, am *azuremonitor.Service, cw *cloudwatch.Service, cm *cloudmonitoring.Service,
es *elasticsearch.Service, grap *graphite.Service, idb *influxdb.Service, lk *loki.Service, otsdb *opentsdb.Service,
pr *prometheus.Service, t *tempo.Service, td *testdatasource.Service, pg *postgres.Service, my *mysql.Service,
ms *mssql.Service, graf *grafanads.Service, pyroscope *pyroscope.Service, parca *parca.Service, zipkin *zipkin.Service, jaeger *jaeger.Service) *Registry {
@ -102,7 +102,7 @@ func ProvideCoreRegistry(tracer tracing.Tracer, am *azuremonitor.Service, cw *cl
sdktracing.InitDefaultTracer(tracer)
return NewRegistry(map[string]backendplugin.PluginFactoryFunc{
CloudWatch: asBackendPlugin(cw.Executor),
CloudWatch: asBackendPlugin(cw),
CloudMonitoring: asBackendPlugin(cm),
AzureMonitor: asBackendPlugin(am),
Elasticsearch: asBackendPlugin(es),
@ -217,7 +217,7 @@ func NewPlugin(pluginID string, cfg *setting.Cfg, httpClientProvider *httpclient
jsonData.AliasIDs = append(jsonData.AliasIDs, TestDataAlias)
svc = testdatasource.ProvideService()
case CloudWatch:
svc = cloudwatch.ProvideService(httpClientProvider).Executor
svc = cloudwatch.ProvideService()
case CloudMonitoring:
svc = cloudmonitoring.ProvideService(httpClientProvider)
case AzureMonitor:

View File

@ -3,8 +3,8 @@ package datasource
import (
"context"
"github.com/grafana/grafana-aws-sdk/pkg/awsauth"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-aws-sdk/pkg/sigv4"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
)
@ -16,8 +16,7 @@ func contextualMiddlewares(ctx context.Context) context.Context {
sigv4Settings := awsds.ReadSigV4Settings(ctx)
if sigv4Settings.Enabled {
authSettings, _ := awsds.ReadAuthSettingsFromContext(ctx)
ctx = httpclient.WithContextualMiddleware(ctx, sigv4.SigV4MiddlewareWithAuthSettings(sigv4Settings.VerboseLogging, *authSettings))
ctx = httpclient.WithContextualMiddleware(ctx, awsauth.NewSigV4Middleware())
}
return ctx

View File

@ -351,7 +351,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser
ossDataSourceRequestURLValidator := validations.ProvideURLValidator()
httpclientProvider := httpclientprovider.New(cfg, ossDataSourceRequestURLValidator, tracingService)
azuremonitorService := azuremonitor.ProvideService(httpclientProvider)
cloudWatchService := cloudwatch.ProvideService(httpclientProvider)
cloudwatchService := cloudwatch.ProvideService()
cloudmonitoringService := cloudmonitoring.ProvideService(httpclientProvider)
elasticsearchService := elasticsearch.ProvideService(httpclientProvider)
graphiteService := graphite.ProvideService(httpclientProvider, tracingService)
@ -446,7 +446,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser
parcaService := parca.ProvideService(httpclientProvider)
zipkinService := zipkin.ProvideService(httpclientProvider)
jaegerService := jaeger.ProvideService(httpclientProvider)
corepluginRegistry := coreplugin.ProvideCoreRegistry(tracingService, azuremonitorService, cloudWatchService, cloudmonitoringService, elasticsearchService, graphiteService, influxdbService, lokiService, opentsdbService, prometheusService, tempoService, testdatasourceService, postgresService, mysqlService, mssqlService, grafanadsService, pyroscopeService, parcaService, zipkinService, jaegerService)
corepluginRegistry := coreplugin.ProvideCoreRegistry(tracingService, azuremonitorService, cloudwatchService, cloudmonitoringService, elasticsearchService, graphiteService, influxdbService, lokiService, opentsdbService, prometheusService, tempoService, testdatasourceService, postgresService, mysqlService, mssqlService, grafanadsService, pyroscopeService, parcaService, zipkinService, jaegerService)
providerService := provider2.ProvideService(corepluginRegistry)
processService := process.ProvideService()
retrieverService := retriever.ProvideService(sqlStore, apikeyService, kvStore, userService, orgService)
@ -861,7 +861,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface {
ossDataSourceRequestURLValidator := validations.ProvideURLValidator()
httpclientProvider := httpclientprovider.New(cfg, ossDataSourceRequestURLValidator, tracingService)
azuremonitorService := azuremonitor.ProvideService(httpclientProvider)
cloudWatchService := cloudwatch.ProvideService(httpclientProvider)
cloudwatchService := cloudwatch.ProvideService()
cloudmonitoringService := cloudmonitoring.ProvideService(httpclientProvider)
elasticsearchService := elasticsearch.ProvideService(httpclientProvider)
graphiteService := graphite.ProvideService(httpclientProvider, tracingService)
@ -956,7 +956,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface {
parcaService := parca.ProvideService(httpclientProvider)
zipkinService := zipkin.ProvideService(httpclientProvider)
jaegerService := jaeger.ProvideService(httpclientProvider)
corepluginRegistry := coreplugin.ProvideCoreRegistry(tracingService, azuremonitorService, cloudWatchService, cloudmonitoringService, elasticsearchService, graphiteService, influxdbService, lokiService, opentsdbService, prometheusService, tempoService, testdatasourceService, postgresService, mysqlService, mssqlService, grafanadsService, pyroscopeService, parcaService, zipkinService, jaegerService)
corepluginRegistry := coreplugin.ProvideCoreRegistry(tracingService, azuremonitorService, cloudwatchService, cloudmonitoringService, elasticsearchService, graphiteService, influxdbService, lokiService, opentsdbService, prometheusService, tempoService, testdatasourceService, postgresService, mysqlService, mssqlService, grafanadsService, pyroscopeService, parcaService, zipkinService, jaegerService)
providerService := provider2.ProvideService(corepluginRegistry)
processService := process.ProvideService()
retrieverService := retriever.ProvideService(sqlStore, apikeyService, kvStore, userService, orgService)

View File

@ -75,7 +75,7 @@ func TestIntegrationPluginManager(t *testing.T) {
hcp := httpclient.NewProvider()
am := azuremonitor.ProvideService(hcp)
cw := cloudwatch.ProvideService(hcp)
cw := cloudwatch.ProvideService()
cm := cloudmonitoring.ProvideService(hcp)
es := elasticsearch.ProvideService(hcp)
grap := graphite.ProvideService(hcp, tracer)

View File

@ -76,25 +76,25 @@ require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/at-wat/mqtt-go v0.19.4 // indirect
github.com/aws/aws-sdk-go v1.55.7 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
github.com/aws/aws-sdk-go-v2 v1.39.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.8 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
github.com/aws/smithy-go v1.20.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 // indirect
github.com/aws/smithy-go v1.23.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
@ -213,7 +213,7 @@ require (
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect
github.com/grafana/grafana-app-sdk v0.35.1 // indirect
github.com/grafana/grafana-app-sdk/logging v0.35.1 // indirect
github.com/grafana/grafana-aws-sdk v0.31.5 // indirect
github.com/grafana/grafana-aws-sdk v0.31.6-0.20250918141554-0b96cca5e46b // indirect
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // indirect
github.com/grafana/grafana-plugin-sdk-go v0.277.0 // indirect
github.com/grafana/grafana/apps/folder v0.0.0-20250414115220-48647355c37b // indirect

View File

@ -740,44 +740,44 @@ github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN
github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
github.com/aws/aws-sdk-go-v2 v1.39.0 h1:xm5WV/2L4emMRmMjHFykqiA4M/ra0DJVSWUkDyBjbg4=
github.com/aws/aws-sdk-go-v2 v1.39.0/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
github.com/aws/aws-sdk-go-v2/config v1.31.8 h1:kQjtOLlTU4m4A64TsRcqwNChhGCwaPBt+zCQt/oWsHU=
github.com/aws/aws-sdk-go-v2/config v1.31.8/go.mod h1:QPpc7IgljrKwH0+E6/KolCgr4WPLerURiU592AYzfSY=
github.com/aws/aws-sdk-go-v2/credentials v1.18.12 h1:zmc9e1q90wMn8wQbjryy8IwA6Q4XlaL9Bx2zIqdNNbk=
github.com/aws/aws-sdk-go-v2/credentials v1.18.12/go.mod h1:3VzdRDR5u3sSJRI4kYcOSIBbeYsgtVk7dG5R/U6qLWY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 h1:Is2tPmieqGS2edBnmOJIbdvOA6Op+rRpaYR60iBAwXM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7/go.mod h1:F1i5V5421EGci570yABvpIXgRIBPb5JM+lSkHF6Dq5w=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 h1:zeN9UtUlA6FTx0vFSayxSX32HDw73Yb6Hh2izDSFxXY=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10/go.mod h1:3HKuexPDcwLWPaqpW2UR/9n8N/u/3CKcGAzSs8p8u8g=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 h1:UCxq0X9O3xrlENdKf1r9eRJoKz/b0AfGkpp3a7FPlhg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7/go.mod h1:rHRoJUNUASj5Z/0eqI4w32vKvC7atoWR0jC+IkmVH8k=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 h1:Y6DTZUn7ZUC4th9FMBbo8LVE+1fyq3ofw+tRwkUd3PY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7/go.mod h1:x3XE6vMnU9QvHN/Wrx2s44kwzV2o2g5x/siw4ZUJ9g8=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 h1:mLgc5QIgOy26qyh5bvW+nDoAppxgn3J2WV3m9ewq7+8=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7/go.mod h1:wXb/eQnqt8mDQIQTTmcw58B5mYGxzLGZGK8PWNFZ0BA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg=
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 h1:hT8ZAZRIfqBqHbzKTII+CIiY8G2oC9OpLedkZ51DWl8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 h1:7PKX3VYsZ8LUWceVRuv0+PU+E7OtQb1lgmi5vmUE9CM=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3/go.mod h1:Ql6jE9kyyWI5JHn+61UT/Y5Z0oyVJGmgmJbZD5g4unY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 h1:e0XBRn3AptQotkyBFrHAxFB8mDhAIOfsG+7KyJ0dg98=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4/go.mod h1:XclEty74bsGBCr1s0VSaA11hQ4ZidK4viWK7rRfO88I=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 h1:PR00NXRYgY4FWHqOGx3fC3lhVKjsp1GdloDv2ynMSd8=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4/go.mod h1:Z+Gd23v97pX9zK97+tX4ppAgqCt3Z2dIXB02CtBncK8=
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=
@ -1281,8 +1281,8 @@ github.com/grafana/grafana-app-sdk v0.35.1 h1:zEXubzsQrxGBOzXJJMBwhEClC/tvPi0sfK
github.com/grafana/grafana-app-sdk v0.35.1/go.mod h1:Zx5MkVppYK+ElSDUAR6+fjzOVo6I/cIgk+ty+LmNOxI=
github.com/grafana/grafana-app-sdk/logging v0.35.1 h1:taVpl+RoixTYl0JBJGhH+fPVmwA9wvdwdzJTZsv9buM=
github.com/grafana/grafana-app-sdk/logging v0.35.1/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk=
github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX/Gh3FZKBE=
github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM=
github.com/grafana/grafana-aws-sdk v0.31.6-0.20250918141554-0b96cca5e46b h1:25XvbNSo8Sgi3MBFOtToRvlbXwfQ44nTb/NYCpZxUJE=
github.com/grafana/grafana-aws-sdk v0.31.6-0.20250918141554-0b96cca5e46b/go.mod h1:2JJbVLc3jjWu0aAQx4bhPa2ChOUjLs9x7ime6mB4O+o=
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 h1:OfCkitCuomzZKW1WYHrG8MxKwtMhALb7jqoj+487eTg=
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo=
github.com/grafana/grafana-plugin-sdk-go v0.277.0 h1:VDU2F4Y5NeRS//ejctdZtsAshrGaEdbtW33FsK0EQss=

View File

@ -58,25 +58,25 @@ require (
github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
github.com/apache/arrow-go/v18 v18.2.0 // indirect
github.com/aws/aws-sdk-go v1.55.7 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
github.com/aws/aws-sdk-go-v2 v1.39.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.8 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
github.com/aws/smithy-go v1.20.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 // indirect
github.com/aws/smithy-go v1.23.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bufbuild/protocompile v0.4.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect

View File

@ -70,44 +70,44 @@ github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE=
github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
github.com/aws/aws-sdk-go-v2 v1.39.0 h1:xm5WV/2L4emMRmMjHFykqiA4M/ra0DJVSWUkDyBjbg4=
github.com/aws/aws-sdk-go-v2 v1.39.0/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
github.com/aws/aws-sdk-go-v2/config v1.31.8 h1:kQjtOLlTU4m4A64TsRcqwNChhGCwaPBt+zCQt/oWsHU=
github.com/aws/aws-sdk-go-v2/config v1.31.8/go.mod h1:QPpc7IgljrKwH0+E6/KolCgr4WPLerURiU592AYzfSY=
github.com/aws/aws-sdk-go-v2/credentials v1.18.12 h1:zmc9e1q90wMn8wQbjryy8IwA6Q4XlaL9Bx2zIqdNNbk=
github.com/aws/aws-sdk-go-v2/credentials v1.18.12/go.mod h1:3VzdRDR5u3sSJRI4kYcOSIBbeYsgtVk7dG5R/U6qLWY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 h1:Is2tPmieqGS2edBnmOJIbdvOA6Op+rRpaYR60iBAwXM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7/go.mod h1:F1i5V5421EGci570yABvpIXgRIBPb5JM+lSkHF6Dq5w=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 h1:zeN9UtUlA6FTx0vFSayxSX32HDw73Yb6Hh2izDSFxXY=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10/go.mod h1:3HKuexPDcwLWPaqpW2UR/9n8N/u/3CKcGAzSs8p8u8g=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 h1:UCxq0X9O3xrlENdKf1r9eRJoKz/b0AfGkpp3a7FPlhg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7/go.mod h1:rHRoJUNUASj5Z/0eqI4w32vKvC7atoWR0jC+IkmVH8k=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 h1:Y6DTZUn7ZUC4th9FMBbo8LVE+1fyq3ofw+tRwkUd3PY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7/go.mod h1:x3XE6vMnU9QvHN/Wrx2s44kwzV2o2g5x/siw4ZUJ9g8=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 h1:mLgc5QIgOy26qyh5bvW+nDoAppxgn3J2WV3m9ewq7+8=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7/go.mod h1:wXb/eQnqt8mDQIQTTmcw58B5mYGxzLGZGK8PWNFZ0BA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg=
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 h1:hT8ZAZRIfqBqHbzKTII+CIiY8G2oC9OpLedkZ51DWl8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 h1:7PKX3VYsZ8LUWceVRuv0+PU+E7OtQb1lgmi5vmUE9CM=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3/go.mod h1:Ql6jE9kyyWI5JHn+61UT/Y5Z0oyVJGmgmJbZD5g4unY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 h1:e0XBRn3AptQotkyBFrHAxFB8mDhAIOfsG+7KyJ0dg98=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4/go.mod h1:XclEty74bsGBCr1s0VSaA11hQ4ZidK4viWK7rRfO88I=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 h1:PR00NXRYgY4FWHqOGx3fC3lhVKjsp1GdloDv2ynMSd8=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4/go.mod h1:Z+Gd23v97pX9zK97+tX4ppAgqCt3Z2dIXB02CtBncK8=
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
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/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=

View File

@ -1,24 +1,45 @@
package routes
package cloudwatch
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-aws-sdk/pkg/awsauth"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
"github.com/patrickmn/go-cache"
"github.com/stretchr/testify/assert"
)
func newTestDatasource(opts ...func(*DataSource)) *DataSource {
ds := &DataSource{
AWSConfigProvider: awsauth.NewFakeConfigProvider(false),
logger: log.NewNullLogger(),
tagValueCache: cache.New(0, 0),
Settings: models.CloudWatchSettings{
AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "us-east-1"},
},
}
ds.resourceHandler = httpadapter.New(ds.newResourceMux())
for _, opt := range opts {
opt(ds)
}
return ds
}
func Test_accounts_route(t *testing.T) {
origNewAccountsService := newAccountsService
ds := newTestDatasource()
origNewAccountsService := services.NewAccountsService
t.Cleanup(func() {
newAccountsService = origNewAccountsService
services.NewAccountsService = origNewAccountsService
})
t.Run("successfully returns array of accounts json", func(t *testing.T) {
@ -31,13 +52,13 @@ func Test_accounts_route(t *testing.T) {
IsMonitoringAccount: true,
},
}}, nil)
newAccountsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.AccountsProvider, error) {
return &mockAccountsService, nil
services.NewAccountsService = func(_ models.OAMAPIProvider) models.AccountsProvider {
return &mockAccountsService
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/accounts?region=us-east-1", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(AccountsHandler, logger, nil))
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.AccountsHandler))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
@ -47,7 +68,7 @@ func Test_accounts_route(t *testing.T) {
t.Run("rejects POST method", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/accounts?region=us-east-1", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(AccountsHandler, logger, nil))
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.AccountsHandler))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusMethodNotAllowed, rr.Code)
})
@ -55,7 +76,7 @@ func Test_accounts_route(t *testing.T) {
t.Run("requires region query value", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/accounts", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(AccountsHandler, logger, nil))
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.AccountsHandler))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
})
@ -64,13 +85,13 @@ func Test_accounts_route(t *testing.T) {
mockAccountsService := mocks.AccountsServiceMock{}
mockAccountsService.On("GetAccountsForCurrentUserOrRole").Return([]resources.ResourceResponse[resources.Account](nil),
fmt.Errorf("%w: %s", services.ErrAccessDeniedException, "some AWS message"))
newAccountsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.AccountsProvider, error) {
return &mockAccountsService, nil
services.NewAccountsService = func(_ models.OAMAPIProvider) models.AccountsProvider {
return &mockAccountsService
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/accounts?region=us-east-1", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(AccountsHandler, logger, nil))
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.AccountsHandler))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusForbidden, rr.Code)
@ -82,13 +103,13 @@ func Test_accounts_route(t *testing.T) {
t.Run("returns 500 when accounts service returns unknown error", func(t *testing.T) {
mockAccountsService := mocks.AccountsServiceMock{}
mockAccountsService.On("GetAccountsForCurrentUserOrRole").Return([]resources.ResourceResponse[resources.Account](nil), fmt.Errorf("some error"))
newAccountsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.AccountsProvider, error) {
return &mockAccountsService, nil
services.NewAccountsService = func(_ models.OAMAPIProvider) models.AccountsProvider {
return &mockAccountsService
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/accounts?region=us-east-1", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(AccountsHandler, logger, nil))
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.AccountsHandler))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)

View File

@ -7,8 +7,10 @@ import (
"strconv"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/kinds/dataquery"
@ -21,7 +23,7 @@ type annotationEvent struct {
Text string
}
func (e *cloudWatchExecutor) executeAnnotationQuery(ctx context.Context, pluginCtx backend.PluginContext, model DataQueryJson, query backend.DataQuery) (*backend.QueryDataResponse, error) {
func (ds *DataSource) executeAnnotationQuery(ctx context.Context, model DataQueryJson, query backend.DataQuery) (*backend.QueryDataResponse, error) {
result := backend.NewQueryDataResponse()
statistic := ""
@ -29,14 +31,14 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(ctx context.Context, pluginC
statistic = *model.Statistic
}
var period int64
var period int32
if model.Period != nil && *model.Period != "" {
p, err := strconv.ParseInt(*model.Period, 10, 64)
p, err := strconv.ParseInt(*model.Period, 10, 32)
if err != nil {
return nil, backend.DownstreamError(fmt.Errorf("query period must be an int"))
}
period = p
period = int32(p)
}
prefixMatching := false
@ -50,7 +52,7 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(ctx context.Context, pluginC
actionPrefix := model.ActionPrefix
alarmNamePrefix := model.AlarmNamePrefix
cli, err := e.getCWClient(ctx, pluginCtx, model.Region)
cli, err := ds.getCWClient(ctx, model.Region)
if err != nil {
result.Responses[query.RefID] = backend.ErrorResponseWithErrorSource(fmt.Errorf("%v: %w", "failed to get client", err))
return result, nil
@ -69,11 +71,11 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(ctx context.Context, pluginC
if prefixMatching {
params := &cloudwatch.DescribeAlarmsInput{
MaxRecords: aws.Int64(100),
MaxRecords: aws.Int32(100),
ActionPrefix: actionPrefix,
AlarmNamePrefix: alarmNamePrefix,
}
resp, err := cli.DescribeAlarms(params)
resp, err := cli.DescribeAlarms(ctx, params)
if err != nil {
result.Responses[query.RefID] = backend.ErrorResponseWithErrorSource(backend.DownstreamError(fmt.Errorf("%v: %w", "failed to call cloudwatch:DescribeAlarms", err)))
return result, nil
@ -84,10 +86,10 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(ctx context.Context, pluginC
return result, backend.DownstreamError(errors.New("invalid annotations query"))
}
var qd []*cloudwatch.Dimension
var qd []cloudwatchtypes.Dimension
for k, v := range dimensions {
for _, vvv := range v.ArrayOfString {
qd = append(qd, &cloudwatch.Dimension{
qd = append(qd, cloudwatchtypes.Dimension{
Name: aws.String(k),
Value: aws.String(vvv),
})
@ -97,10 +99,10 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(ctx context.Context, pluginC
Namespace: aws.String(model.Namespace),
MetricName: aws.String(metricName),
Dimensions: qd,
Statistic: aws.String(statistic),
Period: aws.Int64(period),
Statistic: cloudwatchtypes.Statistic(statistic),
Period: aws.Int32(period),
}
resp, err := cli.DescribeAlarmsForMetric(params)
resp, err := cli.DescribeAlarmsForMetric(ctx, params)
if err != nil {
result.Responses[query.RefID] = backend.ErrorResponseWithErrorSource(backend.DownstreamError(fmt.Errorf("%v: %w", "failed to call cloudwatch:DescribeAlarmsForMetric", err)))
return result, nil
@ -116,9 +118,9 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(ctx context.Context, pluginC
AlarmName: alarmName,
StartDate: aws.Time(query.TimeRange.From),
EndDate: aws.Time(query.TimeRange.To),
MaxRecords: aws.Int64(100),
MaxRecords: aws.Int32(100),
}
resp, err := cli.DescribeAlarmHistory(params)
resp, err := cli.DescribeAlarmHistory(ctx, params)
if err != nil {
result.Responses[query.RefID] = backend.ErrorResponseWithErrorSource(backend.DownstreamError(fmt.Errorf("%v: %w", "failed to call cloudwatch:DescribeAlarmHistory", err)))
return result, nil
@ -127,7 +129,7 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(ctx context.Context, pluginC
annotations = append(annotations, &annotationEvent{
Time: *history.Timestamp,
Title: *history.AlarmName,
Tags: *history.HistoryItemType,
Tags: string(history.HistoryItemType),
Text: *history.HistorySummary,
})
}
@ -162,7 +164,7 @@ func transformAnnotationToTable(annotations []*annotationEvent, query backend.Da
}
func filterAlarms(alarms *cloudwatch.DescribeAlarmsOutput, namespace string, metricName string,
dimensions dataquery.Dimensions, statistic string, period int64) []*string {
dimensions dataquery.Dimensions, statistic string, period int32) []*string {
alarmNames := make([]*string, 0)
for _, alarm := range alarms.MetricAlarms {
@ -189,7 +191,7 @@ func filterAlarms(alarms *cloudwatch.DescribeAlarmsOutput, namespace string, met
continue
}
if *alarm.Statistic != statistic {
if string(alarm.Statistic) != statistic {
continue
}

View File

@ -5,33 +5,31 @@ import (
"encoding/json"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestQuery_AnnotationQuery(t *testing.T) {
ds := newTestDatasource()
origNewCWClient := NewCWClient
t.Cleanup(func() {
NewCWClient = origNewCWClient
})
var client fakeCWAnnotationsClient
NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI {
NewCWClient = func(aws.Config) models.CWClient {
return &client
}
t.Run("DescribeAlarmsForMetric is called with minimum parameters", func(t *testing.T) {
client = fakeCWAnnotationsClient{describeAlarmsForMetricOutput: &cloudwatch.DescribeAlarmsForMetricOutput{}}
im := defaultTestInstanceManager()
executor := newExecutor(im, log.NewNullLogger())
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
@ -53,17 +51,15 @@ func TestQuery_AnnotationQuery(t *testing.T) {
assert.Equal(t, &cloudwatch.DescribeAlarmsForMetricInput{
Namespace: aws.String("custom"),
MetricName: aws.String("CPUUtilization"),
Statistic: aws.String("Average"),
Period: aws.Int64(300),
Statistic: "Average",
Period: aws.Int32(300),
}, client.calls.describeAlarmsForMetric[0])
})
t.Run("DescribeAlarms is called when prefixMatching is true", func(t *testing.T) {
client = fakeCWAnnotationsClient{describeAlarmsOutput: &cloudwatch.DescribeAlarmsOutput{}}
im := defaultTestInstanceManager()
executor := newExecutor(im, log.NewNullLogger())
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
@ -86,7 +82,7 @@ func TestQuery_AnnotationQuery(t *testing.T) {
require.Len(t, client.calls.describeAlarms, 1)
assert.Equal(t, &cloudwatch.DescribeAlarmsInput{
MaxRecords: aws.Int64(100),
MaxRecords: aws.Int32(100),
ActionPrefix: aws.String("some_action_prefix"),
AlarmNamePrefix: aws.String("some_alarm_name_prefix"),
}, client.calls.describeAlarms[0])

View File

@ -1,64 +1,53 @@
package cloudwatch
import (
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/oam"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/oam"
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
)
// NewMetricsAPI is a CloudWatch metrics api factory.
// NewCWClient is a CloudWatch metrics api factory.
//
// Stubbable by tests.
var NewMetricsAPI = func(sess *session.Session) models.CloudWatchMetricsAPIProvider {
return cloudwatch.New(sess)
var NewCWClient = func(cfg aws.Config) models.CWClient {
return cloudwatch.NewFromConfig(cfg)
}
// NewLogsAPI is a CloudWatch logs api factory.
//
// Stubbable by tests.
var NewLogsAPI = func(sess *session.Session) models.CloudWatchLogsAPIProvider {
return cloudwatchlogs.New(sess)
var NewLogsAPI = func(cfg aws.Config) models.CloudWatchLogsAPIProvider {
return cloudwatchlogs.NewFromConfig(cfg)
}
// NewOAMAPI is a CloudWatch OAM api factory.
// NewOAMAPI is a CloudWatch OAM API factory
//
// Stubbable by tests.
var NewOAMAPI = func(sess *session.Session) models.OAMAPIProvider {
return oam.New(sess)
var NewOAMAPI = func(cfg aws.Config) models.OAMAPIProvider {
return oam.NewFromConfig(cfg)
}
// NewCWClient is a CloudWatch client factory.
// NewEC2API is a CloudWatch EC2 API factory
//
// Stubbable by tests.
var NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI {
return cloudwatch.New(sess)
// Stubbable by tests
var NewEC2API = func(cfg aws.Config) models.EC2APIProvider {
return ec2.NewFromConfig(cfg)
}
// NewCWLogsClient is a CloudWatch logs client factory.
//
// Stubbable by tests.
var NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
return cloudwatchlogs.New(sess)
var NewCWLogsClient = func(cfg aws.Config) models.CWLogsClient {
return cloudwatchlogs.NewFromConfig(cfg)
}
// NewEC2Client is a client factory.
// NewRGTAClient is a ResourceGroupsTaggingAPI Client factory.
//
// Stubbable by tests.
var NewEC2Client = func(provider client.ConfigProvider) models.EC2APIProvider {
return ec2.New(provider)
}
// RGTA client factory.
//
// Stubbable by tests.
var newRGTAClient = func(provider client.ConfigProvider) resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI {
return resourcegroupstaggingapi.New(provider)
var NewRGTAClient = func(cfg aws.Config) resourcegroupstaggingapi.GetResourcesAPIClient {
return resourcegroupstaggingapi.NewFromConfig(cfg)
}

View File

@ -3,41 +3,42 @@ package clients
import (
"context"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
)
// this client wraps the CloudWatch API and handles pagination and the composition of the MetricResponse DTO
type metricsClient struct {
models.CloudWatchMetricsAPIProvider
type MetricsClient struct {
cloudwatch.ListMetricsAPIClient
listMetricsPageLimit int
}
func NewMetricsClient(api models.CloudWatchMetricsAPIProvider, pageLimit int) *metricsClient {
return &metricsClient{CloudWatchMetricsAPIProvider: api, listMetricsPageLimit: pageLimit}
func NewMetricsClient(client cloudwatch.ListMetricsAPIClient, listMetricsPageLimit int) *MetricsClient {
return &MetricsClient{
ListMetricsAPIClient: client,
listMetricsPageLimit: listMetricsPageLimit,
}
}
func (l *metricsClient) ListMetricsWithPageLimit(ctx context.Context, params *cloudwatch.ListMetricsInput) ([]resources.MetricResponse, error) {
var cloudWatchMetrics []resources.MetricResponse
pageNum := 0
err := l.ListMetricsPagesWithContext(ctx, params, func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
pageNum++
utils.QueriesTotalCounter.WithLabelValues(utils.ListMetricsLabel).Inc()
metrics, err := awsutil.ValuesAtPath(page, "Metrics")
if err == nil {
for idx, metric := range metrics {
metric := resources.MetricResponse{Metric: metric.(*cloudwatch.Metric)}
if len(page.OwningAccounts) >= idx && params.IncludeLinkedAccounts != nil && *params.IncludeLinkedAccounts {
metric.AccountId = page.OwningAccounts[idx]
}
cloudWatchMetrics = append(cloudWatchMetrics, metric)
}
func (mc *MetricsClient) ListMetricsWithPageLimit(ctx context.Context, params *cloudwatch.ListMetricsInput) ([]resources.MetricResponse, error) {
var responses []resources.MetricResponse
paginator := cloudwatch.NewListMetricsPaginator(mc.ListMetricsAPIClient, params)
includeAccount := params.IncludeLinkedAccounts != nil && *params.IncludeLinkedAccounts
pages := 0
for paginator.HasMorePages() && pages < mc.listMetricsPageLimit {
pages += 1
page, err := paginator.NextPage(ctx)
if err != nil {
return responses, err
}
return !lastPage && pageNum < l.listMetricsPageLimit
})
return cloudWatchMetrics, err
for i, metric := range page.Metrics {
resp := resources.MetricResponse{Metric: metric}
if includeAccount && len(page.OwningAccounts) >= i {
resp.AccountId = &page.OwningAccounts[i]
}
responses = append(responses, resp)
}
}
return responses, nil
}

View File

@ -4,16 +4,19 @@ import (
"context"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMetricsClient(t *testing.T) {
metrics := []*cloudwatch.Metric{
metrics := []cloudwatchtypes.Metric{
{MetricName: aws.String("Test_MetricName1")},
{MetricName: aws.String("Test_MetricName2")},
{MetricName: aws.String("Test_MetricName3")},
@ -50,25 +53,25 @@ func TestMetricsClient(t *testing.T) {
})
t.Run("Should return account id in case IncludeLinkedAccounts is set to true", func(t *testing.T) {
fakeApi := &mocks.FakeMetricsAPI{Metrics: []*cloudwatch.Metric{
fakeApi := &mocks.FakeMetricsAPI{Metrics: []cloudwatchtypes.Metric{
{MetricName: aws.String("Test_MetricName1")},
{MetricName: aws.String("Test_MetricName2")},
{MetricName: aws.String("Test_MetricName3")},
}, OwningAccounts: []*string{aws.String("1234567890"), aws.String("1234567890"), aws.String("1234567895")}}
}, OwningAccounts: []string{"1234567890", "1234567890", "1234567895"}}
client := NewMetricsClient(fakeApi, 100)
response, err := client.ListMetricsWithPageLimit(ctx, &cloudwatch.ListMetricsInput{IncludeLinkedAccounts: aws.Bool(true)})
require.NoError(t, err)
expected := []resources.MetricResponse{
{Metric: &cloudwatch.Metric{MetricName: aws.String("Test_MetricName1")}, AccountId: stringPtr("1234567890")},
{Metric: &cloudwatch.Metric{MetricName: aws.String("Test_MetricName2")}, AccountId: stringPtr("1234567890")},
{Metric: &cloudwatch.Metric{MetricName: aws.String("Test_MetricName3")}, AccountId: stringPtr("1234567895")},
{Metric: cloudwatchtypes.Metric{MetricName: aws.String("Test_MetricName1")}, AccountId: stringPtr("1234567890")},
{Metric: cloudwatchtypes.Metric{MetricName: aws.String("Test_MetricName2")}, AccountId: stringPtr("1234567890")},
{Metric: cloudwatchtypes.Metric{MetricName: aws.String("Test_MetricName3")}, AccountId: stringPtr("1234567895")},
}
assert.Equal(t, expected, response)
})
t.Run("Should not return account id in case IncludeLinkedAccounts is set to false", func(t *testing.T) {
fakeApi := &mocks.FakeMetricsAPI{Metrics: []*cloudwatch.Metric{{MetricName: aws.String("Test_MetricName1")}}, OwningAccounts: []*string{aws.String("1234567890")}}
fakeApi := &mocks.FakeMetricsAPI{Metrics: []cloudwatchtypes.Metric{{MetricName: aws.String("Test_MetricName1")}}, OwningAccounts: []string{"1234567890"}}
client := NewMetricsClient(fakeApi, 100)
response, err := client.ListMetricsWithPageLimit(ctx, &cloudwatch.ListMetricsInput{IncludeLinkedAccounts: aws.Bool(false)})

View File

@ -3,22 +3,19 @@ package cloudwatch
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"slices"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
"github.com/grafana/grafana-aws-sdk/pkg/awsauth"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
@ -37,22 +34,7 @@ const (
// headerFromAlert is used by datasources to identify alert queries
headerFromAlert = "FromAlert"
)
type DataQueryJson struct {
dataquery.CloudWatchAnnotationQuery
Type string `json:"type,omitempty"`
}
type DataSource struct {
Settings models.CloudWatchSettings
HTTPClient *http.Client
sessions SessionCache
tagValueCache *cache.Cache
ProxyOpts *proxy.Options
}
const (
defaultRegion = "default"
logsQueryMode = "Logs"
// QueryTypes
@ -61,74 +43,72 @@ const (
timeSeriesQuery = "timeSeriesQuery"
)
func ProvideService(httpClientProvider *httpclient.Provider) *CloudWatchService {
logger := backend.NewLoggerWith("logger", "tsdb.cloudwatch")
logger.Debug("Initializing")
executor := newExecutor(
datasource.NewInstanceManager(NewInstanceSettings(httpClientProvider)),
logger,
)
return &CloudWatchService{
Executor: executor,
}
type DataQueryJson struct {
dataquery.CloudWatchAnnotationQuery
Type string `json:"type,omitempty"`
}
type CloudWatchService struct {
Executor *cloudWatchExecutor
}
type SessionCache interface {
GetSessionWithAuthSettings(c awsds.GetSessionConfig, as awsds.AuthSettings) (*session.Session, error)
}
func newExecutor(im instancemgmt.InstanceManager, logger log.Logger) *cloudWatchExecutor {
e := &cloudWatchExecutor{
im: im,
logger: logger,
}
e.resourceHandler = httpadapter.New(e.newResourceMux())
return e
}
func NewInstanceSettings(httpClientProvider *httpclient.Provider) datasource.InstanceFactoryFunc {
return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
instanceSettings, err := models.LoadCloudWatchSettings(ctx, settings)
if err != nil {
return nil, fmt.Errorf("error reading settings: %w", err)
}
opts, err := settings.HTTPClientOptions(ctx)
if err != nil {
return nil, err
}
httpClient, err := httpClientProvider.New(opts)
if err != nil {
return nil, fmt.Errorf("error creating http client: %w", err)
}
return DataSource{
Settings: instanceSettings,
HTTPClient: httpClient,
tagValueCache: cache.New(tagValueCacheExpiration, tagValueCacheExpiration*5),
sessions: awsds.NewSessionCache(),
// this is used to build a custom dialer when secure socks proxy is enabled
ProxyOpts: opts.ProxyOptions,
}, nil
}
}
// cloudWatchExecutor executes CloudWatch requests
type cloudWatchExecutor struct {
im instancemgmt.InstanceManager
logger log.Logger
type DataSource struct {
Settings models.CloudWatchSettings
ProxyOpts *proxy.Options
AWSConfigProvider awsauth.ConfigProvider
logger log.Logger
tagValueCache *cache.Cache
resourceHandler backend.CallResourceHandler
}
func (ds *DataSource) newAWSConfig(ctx context.Context, region string) (aws.Config, error) {
if region == defaultRegion || region == "" {
if len(ds.Settings.Region) == 0 {
return aws.Config{}, models.ErrMissingRegion
}
region = ds.Settings.Region
}
authSettings := awsauth.Settings{
CredentialsProfile: ds.Settings.Profile,
LegacyAuthType: ds.Settings.AuthType,
AssumeRoleARN: ds.Settings.AssumeRoleARN,
ExternalID: ds.Settings.ExternalID,
Endpoint: ds.Settings.Endpoint,
Region: region,
AccessKey: ds.Settings.AccessKey,
SecretKey: ds.Settings.SecretKey,
HTTPClient: &http.Client{},
}
if ds.Settings.GrafanaSettings.SecureSocksDSProxyEnabled && ds.Settings.SecureSocksProxyEnabled {
authSettings.ProxyOptions = ds.ProxyOpts
}
cfg, err := ds.AWSConfigProvider.GetConfig(ctx, authSettings)
if err != nil {
return aws.Config{}, err
}
return cfg, nil
}
func NewDatasource(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
instanceSettings, err := models.LoadCloudWatchSettings(ctx, settings)
if err != nil {
return nil, fmt.Errorf("error reading settings: %w", err)
}
opts, err := settings.HTTPClientOptions(ctx)
if err != nil {
return nil, err
}
ds := &DataSource{
Settings: instanceSettings,
// this is used to build a custom dialer when secure socks proxy is enabled
ProxyOpts: opts.ProxyOptions,
AWSConfigProvider: awsauth.NewConfigProvider(),
logger: backend.NewLoggerWith("logger", "grafana-cloudwatch-datasource"),
tagValueCache: cache.New(tagValueCacheExpiration, tagValueCacheExpiration*5),
}
ds.resourceHandler = httpadapter.New(ds.newResourceMux())
return ds, nil
}
// instrumentContext adds plugin key-values to the context; later, logger.FromContext(ctx) will provide a logger
// that adds these values to its output.
// TODO: move this into the sdk (see https://github.com/grafana/grafana/issues/82033)
@ -144,59 +124,12 @@ func instrumentContext(ctx context.Context, endpoint string, pCtx backend.Plugin
return log.WithContextualAttributes(ctx, p)
}
func (e *cloudWatchExecutor) getRequestContext(ctx context.Context, pluginCtx backend.PluginContext, region string) (models.RequestContext, error) {
r := region
instance, err := e.getInstance(ctx, pluginCtx)
if region == defaultRegion {
if err != nil {
return models.RequestContext{}, err
}
r = instance.Settings.Region
}
ec2Client, err := e.getEC2Client(ctx, pluginCtx, defaultRegion)
if err != nil {
return models.RequestContext{}, err
}
sess, err := instance.newSession(r)
if err != nil {
return models.RequestContext{}, err
}
return models.RequestContext{
OAMAPIProvider: NewOAMAPI(sess),
MetricsClientProvider: clients.NewMetricsClient(NewMetricsAPI(sess), instance.Settings.GrafanaSettings.ListMetricsPageLimit),
LogsAPIProvider: NewLogsAPI(sess),
EC2APIProvider: ec2Client,
Settings: instance.Settings,
Logger: e.logger.FromContext(ctx),
}, nil
}
// getRequestContextOnlySettings is useful for resource endpoints that are called before auth has been configured such as external-id that need access to settings but nothing else
func (e *cloudWatchExecutor) getRequestContextOnlySettings(ctx context.Context, pluginCtx backend.PluginContext, _ string) (models.RequestContext, error) {
instance, err := e.getInstance(ctx, pluginCtx)
if err != nil {
return models.RequestContext{}, err
}
return models.RequestContext{
OAMAPIProvider: nil,
MetricsClientProvider: nil,
LogsAPIProvider: nil,
EC2APIProvider: nil,
Settings: instance.Settings,
Logger: e.logger.FromContext(ctx),
}, nil
}
func (e *cloudWatchExecutor) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
func (ds *DataSource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
ctx = instrumentContext(ctx, string(backend.EndpointCallResource), req.PluginContext)
return e.resourceHandler.CallResource(ctx, req, sender)
return ds.resourceHandler.CallResource(ctx, req, sender)
}
func (e *cloudWatchExecutor) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
func (ds *DataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
ctx = instrumentContext(ctx, string(backend.EndpointQueryData), req.PluginContext)
q := req.Queries[0]
var model DataQueryJson
@ -217,37 +150,37 @@ func (e *cloudWatchExecutor) QueryData(ctx context.Context, req *backend.QueryDa
fromPublicDashboard := model.Type == "" && queryMode == logsQueryMode
isSyncLogQuery := ((fromAlert || fromExpression) && queryMode == logsQueryMode) || fromPublicDashboard
if isSyncLogQuery {
return executeSyncLogQuery(ctx, e, req)
return executeSyncLogQuery(ctx, ds, req)
}
var result *backend.QueryDataResponse
switch model.Type {
case annotationQuery:
result, err = e.executeAnnotationQuery(ctx, req.PluginContext, model, q)
result, err = ds.executeAnnotationQuery(ctx, model, q)
case logAction:
result, err = e.executeLogActions(ctx, req)
result, err = ds.executeLogActions(ctx, req)
case timeSeriesQuery:
fallthrough
default:
result, err = e.executeTimeSeriesQuery(ctx, req)
result, err = ds.executeTimeSeriesQuery(ctx, req)
}
return result, err
}
func (e *cloudWatchExecutor) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
func (ds *DataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
ctx = instrumentContext(ctx, string(backend.EndpointCheckHealth), req.PluginContext)
status := backend.HealthStatusOk
metricsTest := "Successfully queried the CloudWatch metrics API."
logsTest := "Successfully queried the CloudWatch logs API."
err := e.checkHealthMetrics(ctx, req.PluginContext)
err := ds.checkHealthMetrics(ctx, req.PluginContext)
if err != nil {
status = backend.HealthStatusError
metricsTest = fmt.Sprintf("CloudWatch metrics query failed: %s", err.Error())
}
err = e.checkHealthLogs(ctx, req.PluginContext)
err = ds.checkHealthLogs(ctx)
if err != nil {
status = backend.HealthStatusError
logsTest = fmt.Sprintf("CloudWatch logs query failed: %s", err.Error())
@ -259,7 +192,7 @@ func (e *cloudWatchExecutor) CheckHealth(ctx context.Context, req *backend.Check
}, nil
}
func (e *cloudWatchExecutor) checkHealthMetrics(ctx context.Context, pluginCtx backend.PluginContext) error {
func (ds *DataSource) checkHealthMetrics(ctx context.Context, _ backend.PluginContext) error {
namespace := "AWS/Billing"
metric := "EstimatedCharges"
params := &cloudwatch.ListMetricsInput{
@ -267,141 +200,75 @@ func (e *cloudWatchExecutor) checkHealthMetrics(ctx context.Context, pluginCtx b
MetricName: &metric,
}
instance, err := e.getInstance(ctx, pluginCtx)
cfg, err := ds.newAWSConfig(ctx, defaultRegion)
if err != nil {
return err
}
session, err := instance.newSession(defaultRegion)
if err != nil {
return err
}
metricClient := clients.NewMetricsClient(NewMetricsAPI(session), instance.Settings.GrafanaSettings.ListMetricsPageLimit)
metricClient := clients.NewMetricsClient(NewCWClient(cfg), ds.Settings.GrafanaSettings.ListMetricsPageLimit)
_, err = metricClient.ListMetricsWithPageLimit(ctx, params)
return err
}
func (e *cloudWatchExecutor) checkHealthLogs(ctx context.Context, pluginCtx backend.PluginContext) error {
session, err := e.newSessionFromContext(ctx, pluginCtx, defaultRegion)
func (ds *DataSource) checkHealthLogs(ctx context.Context) error {
cfg, err := ds.getAWSConfig(ctx, defaultRegion)
if err != nil {
return err
}
logsClient := NewLogsAPI(session)
_, err = logsClient.DescribeLogGroupsWithContext(ctx, &cloudwatchlogs.DescribeLogGroupsInput{Limit: aws.Int64(1)})
logsClient := NewLogsAPI(cfg)
_, err = logsClient.DescribeLogGroups(ctx, &cloudwatchlogs.DescribeLogGroupsInput{Limit: aws.Int32(1)})
return err
}
func (ds *DataSource) newSession(region string) (*session.Session, error) {
if region == defaultRegion {
if len(ds.Settings.Region) == 0 {
return nil, models.ErrMissingRegion
}
region = ds.Settings.Region
}
sess, err := ds.sessions.GetSessionWithAuthSettings(awsds.GetSessionConfig{
// https://github.com/grafana/grafana/issues/46365
// HTTPClient: instance.HTTPClient,
Settings: awsds.AWSDatasourceSettings{
Profile: ds.Settings.Profile,
Region: region,
AuthType: ds.Settings.AuthType,
AssumeRoleARN: ds.Settings.AssumeRoleARN,
ExternalID: ds.Settings.ExternalID,
Endpoint: ds.Settings.Endpoint,
DefaultRegion: ds.Settings.Region,
AccessKey: ds.Settings.AccessKey,
SecretKey: ds.Settings.SecretKey,
},
UserAgentName: aws.String("Cloudwatch")},
ds.Settings.GrafanaSettings)
if err != nil {
return nil, err
}
// work around until https://github.com/grafana/grafana/issues/39089 is implemented
if ds.Settings.GrafanaSettings.SecureSocksDSProxyEnabled && ds.Settings.SecureSocksProxyEnabled {
// only update the transport to try to avoid the issue mentioned here https://github.com/grafana/grafana/issues/46365
// also, 'sess' is cached and reused, so the first time it might have the transport not set, the following uses it will
if sess.Config.HTTPClient.Transport == nil {
// following go standard library logic (https://pkg.go.dev/net/http#Client), if no Transport is provided,
// then we use http.DefaultTransport
defTransport, ok := http.DefaultTransport.(*http.Transport)
if !ok {
// this should not happen but validating just in case
return nil, errors.New("default http client transport is not of type http.Transport")
}
sess.Config.HTTPClient.Transport = defTransport.Clone()
}
err = proxy.New(ds.ProxyOpts).ConfigureSecureSocksHTTPProxy(sess.Config.HTTPClient.Transport.(*http.Transport))
if err != nil {
return nil, fmt.Errorf("error configuring Secure Socks proxy for Transport: %w", err)
}
} else if sess.Config.HTTPClient != nil {
// Workaround for https://github.com/grafana/grafana/issues/91356 - PDC transport set above
// stays on the cached session after PDC is disabled
sess.Config.HTTPClient.Transport = nil
}
return sess, nil
func (ds *DataSource) getAWSConfig(ctx context.Context, region string) (aws.Config, error) {
return ds.newAWSConfig(ctx, region)
}
func (e *cloudWatchExecutor) newSessionFromContext(ctx context.Context, pluginCtx backend.PluginContext, region string) (*session.Session, error) {
instance, err := e.getInstance(ctx, pluginCtx)
func (ds *DataSource) getCWClient(ctx context.Context, region string) (models.CWClient, error) {
cfg, err := ds.getAWSConfig(ctx, region)
if err != nil {
return nil, err
}
return instance.newSession(region)
return NewCWClient(cfg), nil
}
func (e *cloudWatchExecutor) getInstance(ctx context.Context, pluginCtx backend.PluginContext) (*DataSource, error) {
i, err := e.im.Get(ctx, pluginCtx)
func (ds *DataSource) getCWLogsClient(ctx context.Context, region string) (models.CWLogsClient, error) {
cfg, err := ds.getAWSConfig(ctx, region)
if err != nil {
return nil, err
}
instance := i.(DataSource)
return &instance, nil
}
func (e *cloudWatchExecutor) getCWClient(ctx context.Context, pluginCtx backend.PluginContext, region string) (cloudwatchiface.CloudWatchAPI, error) {
sess, err := e.newSessionFromContext(ctx, pluginCtx, region)
if err != nil {
return nil, err
}
return NewCWClient(sess), nil
}
func (e *cloudWatchExecutor) getCWLogsClient(ctx context.Context, pluginCtx backend.PluginContext, region string) (cloudwatchlogsiface.CloudWatchLogsAPI, error) {
sess, err := e.newSessionFromContext(ctx, pluginCtx, region)
if err != nil {
return nil, err
}
logsClient := NewCWLogsClient(sess)
logsClient := NewCWLogsClient(cfg)
return logsClient, nil
}
func (e *cloudWatchExecutor) getEC2Client(ctx context.Context, pluginCtx backend.PluginContext, region string) (models.EC2APIProvider, error) {
sess, err := e.newSessionFromContext(ctx, pluginCtx, region)
func (ds *DataSource) getEC2Client(ctx context.Context, region string) (models.EC2APIProvider, error) {
cfg, err := ds.getAWSConfig(ctx, region)
if err != nil {
return nil, err
}
return NewEC2Client(sess), nil
return NewEC2API(cfg), nil
}
func (e *cloudWatchExecutor) getRGTAClient(ctx context.Context, pluginCtx backend.PluginContext, region string) (resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI,
func (ds *DataSource) getRGTAClient(ctx context.Context, region string) (resourcegroupstaggingapi.GetResourcesAPIClient,
error) {
sess, err := e.newSessionFromContext(ctx, pluginCtx, region)
cfg, err := ds.getAWSConfig(ctx, region)
if err != nil {
return nil, err
}
return newRGTAClient(sess), nil
return NewRGTAClient(cfg), nil
}
func isTerminated(queryStatus string) bool {
return queryStatus == "Complete" || queryStatus == "Cancelled" || queryStatus == "Failed" || queryStatus == "Timeout"
var terminatedStates = []cloudwatchlogstypes.QueryStatus{
cloudwatchlogstypes.QueryStatusComplete,
cloudwatchlogstypes.QueryStatusCancelled,
cloudwatchlogstypes.QueryStatusFailed,
cloudwatchlogstypes.QueryStatusTimeout,
}
func isTerminated(queryStatus cloudwatchlogstypes.QueryStatus) bool {
return slices.Contains(terminatedStates, queryStatus)
}

View File

@ -6,17 +6,13 @@ import (
"net/http"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/aws/aws-sdk-go-v2/aws"
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
@ -27,48 +23,49 @@ import (
func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
sender := &mockedCallResourceResponseSenderForOauth{}
origNewMetricsAPI := NewMetricsAPI
origNewCWClient := NewCWClient
origNewOAMAPI := NewOAMAPI
origNewLogsAPI := NewLogsAPI
origNewEC2Client := NewEC2Client
NewOAMAPI = func(sess *session.Session) models.OAMAPIProvider { return nil }
origNewEC2API := NewEC2API
NewOAMAPI = func(aws.Config) models.OAMAPIProvider { return nil }
var logApi mocks.LogsAPI
NewLogsAPI = func(sess *session.Session) models.CloudWatchLogsAPIProvider {
NewLogsAPI = func(aws.Config) models.CloudWatchLogsAPIProvider {
return &logApi
}
ec2Mock := &mocks.EC2Mock{}
ec2Mock.On("DescribeRegionsWithContext", mock.Anything, mock.Anything).Return(&ec2.DescribeRegionsOutput{}, nil)
NewEC2Client = func(provider client.ConfigProvider) models.EC2APIProvider {
ec2Mock.On("DescribeRegions", mock.Anything, mock.Anything).Return(&ec2.DescribeRegionsOutput{}, nil)
NewEC2API = func(aws.Config) models.EC2APIProvider {
return ec2Mock
}
t.Cleanup(func() {
NewOAMAPI = origNewOAMAPI
NewMetricsAPI = origNewMetricsAPI
NewCWClient = origNewCWClient
NewLogsAPI = origNewLogsAPI
NewEC2Client = origNewEC2Client
NewEC2API = origNewEC2API
})
var api mocks.FakeMetricsAPI
NewMetricsAPI = func(sess *session.Session) models.CloudWatchMetricsAPIProvider {
NewCWClient = func(aws.Config) models.CWClient {
return &api
}
t.Run("Should handle dimension value request and return values from the api", func(t *testing.T) {
im := testInstanceManager(100)
api = mocks.FakeMetricsAPI{Metrics: []*cloudwatch.Metric{
{MetricName: aws.String("Test_MetricName1"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value1")}, {Name: aws.String("Test_DimensionName2"), Value: aws.String("Value2")}}},
{MetricName: aws.String("Test_MetricName2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value3")}}},
{MetricName: aws.String("Test_MetricName3"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName2"), Value: aws.String("Value1")}}},
{MetricName: aws.String("Test_MetricName10"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4"), Value: aws.String("Value2")}, {Name: aws.String("Test_DimensionName5")}}},
{MetricName: aws.String("Test_MetricName4"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName2"), Value: aws.String("Value3")}}},
{MetricName: aws.String("Test_MetricName5"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value4")}}},
{MetricName: aws.String("Test_MetricName6"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value6")}}},
{MetricName: aws.String("Test_MetricName7"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4"), Value: aws.String("Value7")}}},
{MetricName: aws.String("Test_MetricName8"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4"), Value: aws.String("Value1")}}},
{MetricName: aws.String("Test_MetricName9"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value2")}}},
api = mocks.FakeMetricsAPI{Metrics: []cloudwatchtypes.Metric{
{MetricName: aws.String("Test_MetricName1"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value1")}, {Name: aws.String("Test_DimensionName2"), Value: aws.String("Value2")}}},
{MetricName: aws.String("Test_MetricName2"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value3")}}},
{MetricName: aws.String("Test_MetricName3"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName2"), Value: aws.String("Value1")}}},
{MetricName: aws.String("Test_MetricName10"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4"), Value: aws.String("Value2")}, {Name: aws.String("Test_DimensionName5")}}},
{MetricName: aws.String("Test_MetricName4"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName2"), Value: aws.String("Value3")}}},
{MetricName: aws.String("Test_MetricName5"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value4")}}},
{MetricName: aws.String("Test_MetricName6"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value6")}}},
{MetricName: aws.String("Test_MetricName7"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4"), Value: aws.String("Value7")}}},
{MetricName: aws.String("Test_MetricName8"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4"), Value: aws.String("Value1")}}},
{MetricName: aws.String("Test_MetricName9"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value2")}}},
}, MetricsPerPage: 100}
executor := newExecutor(im, log.NewNullLogger())
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.GrafanaSettings.ListMetricsPageLimit = 100
})
req := &backend.CallResourceRequest{
Method: "GET",
@ -78,7 +75,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
PluginID: "cloudwatch",
},
}
err := executor.CallResource(context.Background(), req, sender)
err := ds.CallResource(context.Background(), req, sender)
require.NoError(t, err)
sent := sender.Response
@ -91,20 +88,21 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
})
t.Run("Should handle dimension key filter query and return keys from the api", func(t *testing.T) {
im := testInstanceManager(3)
api = mocks.FakeMetricsAPI{Metrics: []*cloudwatch.Metric{
{MetricName: aws.String("Test_MetricName1"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}, {Name: aws.String("Test_DimensionName2")}}},
{MetricName: aws.String("Test_MetricName2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
{MetricName: aws.String("Test_MetricName3"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName2")}}},
{MetricName: aws.String("Test_MetricName10"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4")}, {Name: aws.String("Test_DimensionName5")}}},
{MetricName: aws.String("Test_MetricName4"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName2")}}},
{MetricName: aws.String("Test_MetricName5"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
{MetricName: aws.String("Test_MetricName6"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
{MetricName: aws.String("Test_MetricName7"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4")}}},
{MetricName: aws.String("Test_MetricName8"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4")}}},
{MetricName: aws.String("Test_MetricName9"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
api = mocks.FakeMetricsAPI{Metrics: []cloudwatchtypes.Metric{
{MetricName: aws.String("Test_MetricName1"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}, {Name: aws.String("Test_DimensionName2")}}},
{MetricName: aws.String("Test_MetricName2"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}}},
{MetricName: aws.String("Test_MetricName3"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName2")}}},
{MetricName: aws.String("Test_MetricName10"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4")}, {Name: aws.String("Test_DimensionName5")}}},
{MetricName: aws.String("Test_MetricName4"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName2")}}},
{MetricName: aws.String("Test_MetricName5"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}}},
{MetricName: aws.String("Test_MetricName6"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}}},
{MetricName: aws.String("Test_MetricName7"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4")}}},
{MetricName: aws.String("Test_MetricName8"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4")}}},
{MetricName: aws.String("Test_MetricName9"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}}},
}, MetricsPerPage: 2}
executor := newExecutor(im, log.NewNullLogger())
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.GrafanaSettings.ListMetricsPageLimit = 3
})
req := &backend.CallResourceRequest{
Method: "GET",
@ -114,7 +112,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
PluginID: "cloudwatch",
},
}
err := executor.CallResource(context.Background(), req, sender)
err := ds.CallResource(context.Background(), req, sender)
require.NoError(t, err)
sent := sender.Response
@ -127,9 +125,10 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
})
t.Run("Should handle standard dimension key query and return hard coded keys", func(t *testing.T) {
im := defaultTestInstanceManager()
api = mocks.FakeMetricsAPI{}
executor := newExecutor(im, log.NewNullLogger())
ds := newTestDatasource(func(ds *DataSource) {
})
req := &backend.CallResourceRequest{
Method: "GET",
@ -139,7 +138,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
PluginID: "cloudwatch",
},
}
err := executor.CallResource(context.Background(), req, sender)
err := ds.CallResource(context.Background(), req, sender)
require.NoError(t, err)
sent := sender.Response
@ -152,9 +151,8 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
})
t.Run("Should handle custom namespace dimension key query and return hard coded keys", func(t *testing.T) {
im := defaultTestInstanceManager()
api = mocks.FakeMetricsAPI{}
executor := newExecutor(im, log.NewNullLogger())
ds := newTestDatasource()
req := &backend.CallResourceRequest{
Method: "GET",
@ -164,7 +162,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
PluginID: "cloudwatch",
},
}
err := executor.CallResource(context.Background(), req, sender)
err := ds.CallResource(context.Background(), req, sender)
require.NoError(t, err)
sent := sender.Response
@ -177,20 +175,21 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
})
t.Run("Should handle custom namespace metrics query and return metrics from api", func(t *testing.T) {
im := testInstanceManager(3)
api = mocks.FakeMetricsAPI{Metrics: []*cloudwatch.Metric{
{MetricName: aws.String("Test_MetricName1"), Namespace: aws.String("AWS/EC2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}, {Name: aws.String("Test_DimensionName2")}}},
{MetricName: aws.String("Test_MetricName2"), Namespace: aws.String("AWS/EC2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
{MetricName: aws.String("Test_MetricName3"), Namespace: aws.String("AWS/ECS"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName2")}}},
{MetricName: aws.String("Test_MetricName10"), Namespace: aws.String("AWS/ECS"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4")}, {Name: aws.String("Test_DimensionName5")}}},
{MetricName: aws.String("Test_MetricName4"), Namespace: aws.String("AWS/ECS"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName2")}}},
{MetricName: aws.String("Test_MetricName5"), Namespace: aws.String("AWS/Redshift"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
{MetricName: aws.String("Test_MetricName6"), Namespace: aws.String("AWS/Redshift"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
{MetricName: aws.String("Test_MetricName7"), Namespace: aws.String("AWS/EC2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4")}}},
{MetricName: aws.String("Test_MetricName8"), Namespace: aws.String("AWS/EC2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4")}}},
{MetricName: aws.String("Test_MetricName9"), Namespace: aws.String("AWS/EC2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
api = mocks.FakeMetricsAPI{Metrics: []cloudwatchtypes.Metric{
{MetricName: aws.String("Test_MetricName1"), Namespace: aws.String("AWS/EC2"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}, {Name: aws.String("Test_DimensionName2")}}},
{MetricName: aws.String("Test_MetricName2"), Namespace: aws.String("AWS/EC2"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}}},
{MetricName: aws.String("Test_MetricName3"), Namespace: aws.String("AWS/ECS"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName2")}}},
{MetricName: aws.String("Test_MetricName10"), Namespace: aws.String("AWS/ECS"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4")}, {Name: aws.String("Test_DimensionName5")}}},
{MetricName: aws.String("Test_MetricName4"), Namespace: aws.String("AWS/ECS"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName2")}}},
{MetricName: aws.String("Test_MetricName5"), Namespace: aws.String("AWS/Redshift"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}}},
{MetricName: aws.String("Test_MetricName6"), Namespace: aws.String("AWS/Redshift"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}}},
{MetricName: aws.String("Test_MetricName7"), Namespace: aws.String("AWS/EC2"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4")}}},
{MetricName: aws.String("Test_MetricName8"), Namespace: aws.String("AWS/EC2"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4")}}},
{MetricName: aws.String("Test_MetricName9"), Namespace: aws.String("AWS/EC2"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}}},
}, MetricsPerPage: 2}
executor := newExecutor(im, log.NewNullLogger())
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.GrafanaSettings.ListMetricsPageLimit = 3
})
req := &backend.CallResourceRequest{
Method: "GET",
@ -200,7 +199,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
PluginID: "cloudwatch",
},
}
err := executor.CallResource(context.Background(), req, sender)
err := ds.CallResource(context.Background(), req, sender)
require.NoError(t, err)
sent := sender.Response
@ -213,21 +212,20 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
})
t.Run("Should handle log group fields request", func(t *testing.T) {
im := defaultTestInstanceManager()
logApi = mocks.LogsAPI{}
logApi.On("GetLogGroupFieldsWithContext", mock.Anything).Return(&cloudwatchlogs.GetLogGroupFieldsOutput{
LogGroupFields: []*cloudwatchlogs.LogGroupField{
logApi.On("GetLogGroupFields", mock.Anything).Return(&cloudwatchlogs.GetLogGroupFieldsOutput{
LogGroupFields: []cloudwatchlogstypes.LogGroupField{
{
Name: aws.String("field1"),
Percent: aws.Int64(50),
Percent: 50,
},
{
Name: aws.String("field2"),
Percent: aws.Int64(50),
Percent: 50,
},
},
}, nil)
executor := newExecutor(im, log.NewNullLogger())
ds := newTestDatasource()
req := &backend.CallResourceRequest{
Method: "GET",
@ -237,7 +235,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
PluginID: "cloudwatch",
},
}
err := executor.CallResource(context.Background(), req, sender)
err := ds.CallResource(context.Background(), req, sender)
require.NoError(t, err)
sent := sender.Response
@ -248,8 +246,9 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
})
t.Run("Should handle region requests and return regions from the api", func(t *testing.T) {
im := defaultTestInstanceManager()
executor := newExecutor(im, log.NewNullLogger())
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.Region = "us-east-2"
})
req := &backend.CallResourceRequest{
Method: "GET",
Path: `/regions`,
@ -258,7 +257,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
PluginID: "cloudwatch",
},
}
err := executor.CallResource(context.Background(), req, sender)
err := ds.CallResource(context.Background(), req, sender)
require.NoError(t, err)
sent := sender.Response
require.NotNil(t, sent)
@ -268,14 +267,11 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
})
t.Run("Should error for any request when a default region is not selected", func(t *testing.T) {
imWithoutDefaultRegion := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{
AWSDatasourceSettings: awsds.AWSDatasourceSettings{},
GrafanaSettings: awsds.AuthSettings{ListMetricsPageLimit: 1000},
}}, nil
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.GrafanaSettings.ListMetricsPageLimit = 1000
ds.Settings.Region = ""
})
executor := newExecutor(imWithoutDefaultRegion, log.NewNullLogger())
req := &backend.CallResourceRequest{
Method: "GET",
Path: `/regions`,
@ -284,7 +280,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
PluginID: "cloudwatch",
},
}
err := executor.CallResource(context.Background(), req, sender)
err := ds.CallResource(context.Background(), req, sender)
require.NoError(t, err)
sent := sender.Response
require.NotNil(t, sent)

View File

@ -6,18 +6,15 @@ import (
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
awsclient "github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
"github.com/google/go-cmp/cmp"
"github.com/grafana/grafana-aws-sdk/pkg/awsauth"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
@ -34,7 +31,7 @@ func TestNewInstanceSettings(t *testing.T) {
name string
settings backend.DataSourceInstanceSettings
settingCtx context.Context
expectedDS DataSource
expectedDS *DataSource
Err require.ErrorAssertionFunc
}{
{
@ -62,7 +59,7 @@ func TestNewInstanceSettings(t *testing.T) {
awsds.ListMetricsPageLimitKeyName: "50",
proxy.PluginSecureSocksProxyEnabled: "true",
})),
expectedDS: DataSource{
expectedDS: &DataSource{
Settings: models.CloudWatchSettings{
AWSDatasourceSettings: awsds.AWSDatasourceSettings{
Profile: "foo",
@ -91,11 +88,11 @@ func TestNewInstanceSettings(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := NewInstanceSettings(httpclient.NewProvider())
model, err := f(tt.settingCtx, tt.settings)
instance, err := NewDatasource(tt.settingCtx, tt.settings)
ds := instance.(*DataSource)
tt.Err(t, err)
assert.Equal(t, tt.expectedDS.Settings.GrafanaSettings, model.(DataSource).Settings.GrafanaSettings)
datasourceComparer := cmp.Comparer(func(d1 DataSource, d2 DataSource) bool {
assert.Equal(t, tt.expectedDS.Settings.GrafanaSettings, ds.Settings.GrafanaSettings)
datasourceComparer := cmp.Comparer(func(d1 *DataSource, d2 *DataSource) bool {
return d1.Settings.Profile == d2.Settings.Profile &&
d1.Settings.Region == d2.Settings.Region &&
d1.Settings.AuthType == d2.Settings.AuthType &&
@ -106,40 +103,39 @@ func TestNewInstanceSettings(t *testing.T) {
d1.Settings.AccessKey == d2.Settings.AccessKey &&
d1.Settings.SecretKey == d2.Settings.SecretKey
})
if !cmp.Equal(model.(DataSource), tt.expectedDS, datasourceComparer) {
t.Errorf("Unexpected result. Expecting\n%v \nGot:\n%v", model, tt.expectedDS)
if !cmp.Equal(instance.(*DataSource), tt.expectedDS, datasourceComparer) {
t.Errorf("Unexpected result. Expecting\n%v \nGot:\n%v", instance, tt.expectedDS)
}
})
}
}
func Test_CheckHealth(t *testing.T) {
origNewMetricsAPI := NewMetricsAPI
origNewCWClient := NewCWClient
origNewCWLogsClient := NewCWLogsClient
origNewLogsAPI := NewLogsAPI
t.Cleanup(func() {
NewMetricsAPI = origNewMetricsAPI
NewCWClient = origNewCWClient
NewCWLogsClient = origNewCWLogsClient
NewLogsAPI = origNewLogsAPI
})
var client fakeCheckHealthClient
NewMetricsAPI = func(sess *session.Session) models.CloudWatchMetricsAPIProvider {
NewCWClient = func(aws.Config) models.CWClient {
return client
}
NewLogsAPI = func(sess *session.Session) models.CloudWatchLogsAPIProvider {
NewLogsAPI = func(aws.Config) models.CloudWatchLogsAPIProvider {
return client
}
im := defaultTestInstanceManager()
t.Run("successfully query metrics and logs", func(t *testing.T) {
client = fakeCheckHealthClient{}
executor := newExecutor(im, log.NewNullLogger())
resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.Region = "us-east-1"
})
resp, err := ds.CheckHealth(context.Background(), &backend.CheckHealthRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}})
assert.NoError(t, err)
assert.Equal(t, &backend.CheckHealthResult{
@ -149,14 +145,15 @@ func Test_CheckHealth(t *testing.T) {
})
t.Run("successfully queries metrics, fails during logs query", func(t *testing.T) {
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.Region = "us-east-1"
})
client = fakeCheckHealthClient{
describeLogGroups: func(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
describeLogGroupsFunction: func(context.Context, *cloudwatchlogs.DescribeLogGroupsInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
return nil, fmt.Errorf("some logs query error")
}}
executor := newExecutor(im, log.NewNullLogger())
resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{
},
}
resp, err := ds.CheckHealth(context.Background(), &backend.CheckHealthRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
})
@ -168,14 +165,15 @@ func Test_CheckHealth(t *testing.T) {
})
t.Run("successfully queries logs, fails during metrics query", func(t *testing.T) {
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.Region = "us-east-1"
ds.Settings.GrafanaSettings.ListMetricsPageLimit = 1
})
client = fakeCheckHealthClient{
listMetricsPages: func(input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool) error {
return fmt.Errorf("some list metrics error")
listMetricsFunction: func(context.Context, *cloudwatch.ListMetricsInput, ...func(*cloudwatch.Options)) (*cloudwatch.ListMetricsOutput, error) {
return nil, fmt.Errorf("some list metrics error")
}}
executor := newExecutor(im, log.NewNullLogger())
resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{
resp, err := ds.CheckHealth(context.Background(), &backend.CheckHealthRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
})
@ -188,30 +186,25 @@ func Test_CheckHealth(t *testing.T) {
t.Run("fail to get clients", func(t *testing.T) {
client = fakeCheckHealthClient{}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{
Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "us-east-1"}},
sessions: &fakeSessionCache{getSessionWithAuthSettings: func(c awsds.GetSessionConfig, a awsds.AuthSettings) (*session.Session, error) {
return nil, fmt.Errorf("some sessions error")
}},
}, nil
ds := newTestDatasource(func(ds *DataSource) {
ds.AWSConfigProvider = awsauth.NewFakeConfigProvider(true)
ds.Settings.Region = "us-east-1"
})
executor := newExecutor(im, log.NewNullLogger())
resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{
resp, err := ds.CheckHealth(context.Background(), &backend.CheckHealthRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
})
assert.NoError(t, err)
assert.Equal(t, &backend.CheckHealthResult{
Status: backend.HealthStatusError,
Message: "1. CloudWatch metrics query failed: some sessions error\n2. CloudWatch logs query failed: some sessions error",
Message: "1. CloudWatch metrics query failed: LoadDefaultConfig failed\n2. CloudWatch logs query failed: LoadDefaultConfig failed",
}, resp)
})
}
func TestNewSession_passes_authSettings(t *testing.T) {
func TestGetAWSConfig_passes_authSettings(t *testing.T) {
// TODO: update this for the new auth structure, or remove it
t.Skip()
ctxDuration := 15 * time.Minute
expectedSettings := awsds.AuthSettings{
AllowedAuthProviders: []string{"foo", "bar", "baz"},
@ -221,56 +214,40 @@ func TestNewSession_passes_authSettings(t *testing.T) {
ListMetricsPageLimit: 50,
SecureSocksDSProxyEnabled: true,
}
im := datasource.NewInstanceManager((func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{
Settings: models.CloudWatchSettings{
AWSDatasourceSettings: awsds.AWSDatasourceSettings{
Region: "us-east-1",
},
GrafanaSettings: expectedSettings,
},
sessions: &fakeSessionCache{getSessionWithAuthSettings: func(c awsds.GetSessionConfig, a awsds.AuthSettings) (*session.Session, error) {
assert.Equal(t, expectedSettings, a)
return &session.Session{
Config: &aws.Config{},
}, nil
}},
}, nil
}))
executor := newExecutor(im, log.NewNullLogger())
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.Region = "us-east-1"
ds.Settings.GrafanaSettings = expectedSettings
})
_, err := executor.newSessionFromContext(context.Background(),
backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, "us-east-1")
_, err := ds.getAWSConfig(context.Background(), "us-east-1")
require.NoError(t, err)
}
func TestQuery_ResourceRequest_DescribeLogGroups_with_CrossAccountQuerying(t *testing.T) {
sender := &mockedCallResourceResponseSenderForOauth{}
origNewMetricsAPI := NewMetricsAPI
origNewMetricsAPI := NewCWClient
origNewOAMAPI := NewOAMAPI
origNewLogsAPI := NewLogsAPI
origNewEC2Client := NewEC2Client
NewMetricsAPI = func(sess *session.Session) models.CloudWatchMetricsAPIProvider { return nil }
NewOAMAPI = func(sess *session.Session) models.OAMAPIProvider { return nil }
NewEC2Client = func(provider awsclient.ConfigProvider) models.EC2APIProvider { return nil }
origNewEC2API := NewEC2API
NewCWClient = func(aws.Config) models.CWClient { return nil }
NewOAMAPI = func(aws.Config) models.OAMAPIProvider { return nil }
NewEC2API = func(aws.Config) models.EC2APIProvider { return nil }
t.Cleanup(func() {
NewOAMAPI = origNewOAMAPI
NewMetricsAPI = origNewMetricsAPI
NewCWClient = origNewMetricsAPI
NewLogsAPI = origNewLogsAPI
NewEC2Client = origNewEC2Client
NewEC2API = origNewEC2API
})
var logsApi mocks.LogsAPI
NewLogsAPI = func(sess *session.Session) models.CloudWatchLogsAPIProvider {
NewLogsAPI = func(aws.Config) models.CloudWatchLogsAPIProvider {
return &logsApi
}
im := defaultTestInstanceManager()
t.Run("maps log group api response to resource response of log-groups", func(t *testing.T) {
logsApi = mocks.LogsAPI{}
logsApi.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{
LogGroups: []*cloudwatchlogs.LogGroup{
logsApi.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{
LogGroups: []cloudwatchlogstypes.LogGroup{
{Arn: aws.String("arn:aws:logs:us-east-1:111:log-group:group_a"), LogGroupName: aws.String("group_a")},
},
}, nil)
@ -283,8 +260,9 @@ func TestQuery_ResourceRequest_DescribeLogGroups_with_CrossAccountQuerying(t *te
},
}
executor := newExecutor(im, log.NewNullLogger())
err := executor.CallResource(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), req, sender)
ds := newTestDatasource()
err := ds.CallResource(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), req, sender)
assert.NoError(t, err)
assert.JSONEq(t, `[
@ -297,11 +275,11 @@ func TestQuery_ResourceRequest_DescribeLogGroups_with_CrossAccountQuerying(t *te
}
]`, string(sender.Response.Body))
logsApi.AssertCalled(t, "DescribeLogGroupsWithContext",
logsApi.AssertCalled(t, "DescribeLogGroups",
&cloudwatchlogs.DescribeLogGroupsInput{
AccountIdentifiers: []*string{utils.Pointer("some-account-id")},
AccountIdentifiers: []string{"some-account-id"},
IncludeLinkedAccounts: utils.Pointer(true),
Limit: utils.Pointer(int64(50)),
Limit: aws.Int32(50),
LogGroupNamePrefix: utils.Pointer("some-pattern"),
})
})

View File

@ -1,21 +1,12 @@
package constants
import "github.com/grafana/grafana-aws-sdk/pkg/cloudWatchConsts"
// NamespaceMetricsMap is a map of Cloudwatch namespaces to their metrics
// Deprecated: use cloudWatchConsts.NamespaceMetricsMap from grafana-aws-sdk instead
var NamespaceMetricsMap = cloudWatchConsts.NamespaceMetricsMap
// NamespaceDimensionKeysMap is a map of CloudWatch namespaces to their dimension keys
// Deprecated: use cloudWatchConsts.NamespaceDimensionKeysMap from grafana-aws-sdk instead
var NamespaceDimensionKeysMap = cloudWatchConsts.NamespaceDimensionKeysMap
type RegionsSet map[string]struct{}
func Regions() RegionsSet {
return RegionsSet{
"af-south-1": {},
"ap-east-1": {},
"ap-east-2": {},
"ap-northeast-1": {},
"ap-northeast-2": {},
"ap-northeast-3": {},
@ -25,7 +16,10 @@ func Regions() RegionsSet {
"ap-southeast-2": {},
"ap-southeast-3": {},
"ap-southeast-4": {},
"ap-southeast-5": {},
"ap-southeast-7": {},
"ca-central-1": {},
"ca-west-1": {},
"cn-north-1": {},
"cn-northwest-1": {},
"eu-central-1": {},
@ -39,6 +33,7 @@ func Regions() RegionsSet {
"il-central-1": {},
"me-central-1": {},
"me-south-1": {},
"mx-central-1": {},
"sa-east-1": {},
"us-east-1": {},
"us-east-2": {},

View File

@ -1,15 +1,12 @@
package routes
package cloudwatch
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@ -20,11 +17,19 @@ import (
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
)
var logger = log.NewNullLogger()
func Test_DimensionKeys_Route(t *testing.T) {
origNewListMetricsService := services.NewListMetricsService
t.Cleanup(func() {
services.NewListMetricsService = origNewListMetricsService
})
var mockListMetricsService mocks.ListMetricsServiceMock
services.NewListMetricsService = func(models.MetricsClientProvider) models.ListMetricsProvider {
return &mockListMetricsService
}
t.Run("calls FilterDimensionKeysRequest when a StandardDimensionKeysRequest is passed", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService = mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetDimensionKeysByDimensionFilter", mock.MatchedBy(func(r resources.DimensionKeysRequest) bool {
return r.ResourceRequest != nil && *r.ResourceRequest == resources.ResourceRequest{Region: "us-east-2"} &&
r.Namespace == "AWS/EC2" &&
@ -33,12 +38,10 @@ func Test_DimensionKeys_Route(t *testing.T) {
assert.Contains(t, r.DimensionFilter, &resources.Dimension{Name: "NodeID", Value: "Shared"}) &&
assert.Contains(t, r.DimensionFilter, &resources.Dimension{Name: "stage", Value: "QueryCommit"})
})).Return([]resources.ResourceResponse[string]{}, nil).Once()
newListMetricsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", `/dimension-keys?region=us-east-2&namespace=AWS/EC2&metricName=CPUUtilization&dimensionFilters={"NodeID":["Shared"],"stage":["QueryCommit"]}`, nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, logger, nil))
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.DimensionKeysHandler))
handler.ServeHTTP(rr, req)
})
@ -56,7 +59,8 @@ func Test_DimensionKeys_Route(t *testing.T) {
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/dimension-keys?region=us-east-2&namespace=AWS/EC2&metricName=CPUUtilization", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, logger, nil))
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.DimensionKeysHandler))
handler.ServeHTTP(rr, req)
res := []resources.Metric{}
err := json.Unmarshal(rr.Body.Bytes(), &res)
@ -66,14 +70,12 @@ func Test_DimensionKeys_Route(t *testing.T) {
})
t.Run("return 500 if GetDimensionKeysByDimensionFilter returns an error", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService = mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetDimensionKeysByDimensionFilter", mock.Anything).Return([]resources.ResourceResponse[string]{}, fmt.Errorf("some error"))
newListMetricsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", `/dimension-keys?region=us-east-2&namespace=AWS/EC2&metricName=CPUUtilization&dimensionFilters={"NodeID":["Shared"],"stage":["QueryCommit"]}`, nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, logger, nil))
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.DimensionKeysHandler))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
assert.Equal(t, `{"Message":"error in DimensionKeyHandler: some error","Error":"some error","StatusCode":500}`, rr.Body.String())

View File

@ -1,14 +1,13 @@
package routes
package cloudwatch
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@ -17,8 +16,18 @@ import (
)
func Test_DimensionValues_Route(t *testing.T) {
origNewListMetricsService := services.NewListMetricsService
t.Cleanup(func() {
services.NewListMetricsService = origNewListMetricsService
})
var mockListMetricsService mocks.ListMetricsServiceMock
services.NewListMetricsService = func(models.MetricsClientProvider) models.ListMetricsProvider {
return &mockListMetricsService
}
t.Run("Calls GetDimensionValuesByDimensionFilter when a valid request is passed", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService = mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetDimensionValuesByDimensionFilter", mock.MatchedBy(func(r resources.DimensionValuesRequest) bool {
return r.ResourceRequest != nil && *r.ResourceRequest == resources.ResourceRequest{Region: "us-east-2"} &&
r.Namespace == "AWS/EC2" &&
@ -28,24 +37,20 @@ func Test_DimensionValues_Route(t *testing.T) {
assert.Contains(t, r.DimensionFilter, &resources.Dimension{Name: "NodeID", Value: "Shared"}) &&
assert.Contains(t, r.DimensionFilter, &resources.Dimension{Name: "stage", Value: "QueryCommit"})
})).Return([]resources.ResourceResponse[string]{}, nil).Once()
newListMetricsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", `/dimension-values?region=us-east-2&dimensionKey=instanceId&namespace=AWS/EC2&metricName=CPUUtilization&dimensionFilters={"NodeID":["Shared"],"stage":["QueryCommit"]}`, nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionValuesHandler, logger, nil))
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.DimensionValuesHandler))
handler.ServeHTTP(rr, req)
})
t.Run("returns 500 if GetDimensionValuesByDimensionFilter returns an error", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService = mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetDimensionValuesByDimensionFilter", mock.Anything).Return([]resources.ResourceResponse[string]{}, fmt.Errorf("some error"))
newListMetricsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", `/dimension-values?region=us-east-2&dimensionKey=instanceId&namespace=AWS/EC2&metricName=CPUUtilization&dimensionFilters={"NodeID":["Shared"],"stage":["QueryCommit"]}`, nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionValuesHandler, logger, nil))
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.DimensionValuesHandler))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
assert.Equal(t, `{"Message":"error in DimensionValuesHandler: some error","Error":"some error","StatusCode":500}`, rr.Body.String())

View File

@ -0,0 +1,40 @@
package cloudwatch
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_external_id_route(t *testing.T) {
t.Run("successfully returns an external id from the instance", func(t *testing.T) {
t.Setenv("AWS_AUTH_EXTERNAL_ID", "mock-external-id")
rr := httptest.NewRecorder()
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.GrafanaSettings.ExternalID = "mock-external-id"
})
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.ExternalIdHandler))
req := httptest.NewRequest("GET", "/external-id", nil)
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.JSONEq(t, `{"externalId":"mock-external-id"}`, rr.Body.String())
})
t.Run("returns an empty string if there is no external id", func(t *testing.T) {
rr := httptest.NewRecorder()
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.ExternalIdHandler))
req := httptest.NewRequest("GET", "/external-id", nil)
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.JSONEq(t, `{"externalId":""}`, rr.Body.String())
})
}

View File

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
"github.com/patrickmn/go-cache"
)
@ -26,10 +27,10 @@ func shouldSkipFetchingWildcards(ctx context.Context, q *models.CloudWatchQuery)
}
// getDimensionValues gets the actual dimension values for dimensions with a wildcard
func (e *cloudWatchExecutor) getDimensionValuesForWildcards(
func (ds *DataSource) getDimensionValuesForWildcards(
ctx context.Context,
region string,
client models.CloudWatchMetricsAPIProvider,
client models.CWClient,
origQueries []*models.CloudWatchQuery,
tagValueCache *cache.Cache,
listMetricsPageLimit int,
@ -57,12 +58,12 @@ func (e *cloudWatchExecutor) getDimensionValuesForWildcards(
cacheKey := fmt.Sprintf("%s-%s-%s-%s-%s", region, accountID, query.Namespace, query.MetricName, dimensionKey)
cachedDimensions, found := tagValueCache.Get(cacheKey)
if found {
e.logger.FromContext(ctx).Debug("Fetching dimension values from cache")
ds.logger.FromContext(ctx).Debug("Fetching dimension values from cache")
query.Dimensions[dimensionKey] = cachedDimensions.([]string)
continue
}
e.logger.FromContext(ctx).Debug("Cache miss, fetching dimension values from AWS")
ds.logger.FromContext(ctx).Debug("Cache miss, fetching dimension values from AWS")
request := resources.DimensionValuesRequest{
ResourceRequest: &resources.ResourceRequest{
Region: region,

View File

@ -4,8 +4,8 @@ import (
"context"
"testing"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/kinds/dataquery"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
@ -14,10 +14,10 @@ import (
"github.com/stretchr/testify/assert"
)
func noSkip(ctx context.Context, q *models.CloudWatchQuery) bool { return false }
func noSkip(context.Context, *models.CloudWatchQuery) bool { return false }
func TestGetDimensionValuesForWildcards(t *testing.T) {
executor := &cloudWatchExecutor{im: defaultTestInstanceManager(), logger: log.NewNullLogger()}
ds := newTestDatasource()
ctx := context.Background()
t.Run("Tag value cache", func(t *testing.T) {
@ -29,17 +29,17 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
query.Dimensions = map[string][]string{"Test_DimensionName": {"*"}}
query.MetricQueryType = models.MetricQueryTypeSearch
query.MatchExact = false
api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName"), Value: utils.Pointer("Value")}}},
api := &mocks.MetricsAPI{Metrics: []cloudwatchtypes.Metric{
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName"), Value: utils.Pointer("Value")}}},
}}
api.On("ListMetricsPagesWithContext").Return(nil)
_, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
api.On("ListMetrics").Return(nil)
_, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
assert.Nil(t, err)
// make sure the original query wasn't altered
assert.Equal(t, map[string][]string{"Test_DimensionName": {"*"}}, query.Dimensions)
//setting the api to nil confirms that it's using the cached value
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
assert.Nil(t, err)
assert.Len(t, queries, 1)
assert.Equal(t, map[string][]string{"Test_DimensionName": {"Value"}}, queries[0].Dimensions)
@ -52,20 +52,20 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
query.Dimensions = map[string][]string{"Test_DimensionName2": {"*"}}
query.MetricQueryType = models.MetricQueryTypeSearch
query.MatchExact = false
api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{}}
api.On("ListMetricsPagesWithContext").Return(nil)
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
api := &mocks.MetricsAPI{Metrics: []cloudwatchtypes.Metric{}}
api.On("ListMetrics").Return(nil)
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
assert.Nil(t, err)
assert.Len(t, queries, 1)
// assert that the values was set to an empty array
assert.Equal(t, map[string][]string{"Test_DimensionName2": {}}, queries[0].Dimensions)
// Confirm that it calls the api again if the last call did not return any values
api.Metrics = []*cloudwatch.Metric{
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Value")}}},
api.Metrics = []cloudwatchtypes.Metric{
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Value")}}},
}
api.On("ListMetricsPagesWithContext").Return(nil)
queries, err = executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
api.On("ListMetrics").Return(nil)
queries, err = ds.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
assert.Nil(t, err)
assert.Len(t, queries, 1)
assert.Equal(t, map[string][]string{"Test_DimensionName2": {"Value"}}, queries[0].Dimensions)
@ -81,7 +81,7 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}}
query.MetricQueryType = models.MetricQueryTypeSearch
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
assert.Nil(t, err)
assert.Len(t, queries, 1)
assert.Equal(t, []string{"*"}, queries[0].Dimensions["Test_DimensionName1"])
@ -93,7 +93,7 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}}
query.MetricQueryType = models.MetricQueryTypeSearch
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
assert.Nil(t, err)
assert.Len(t, queries, 1)
assert.Equal(t, []string{"*"}, queries[0].Dimensions["Test_DimensionName1"])
@ -107,7 +107,7 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
query.Dimensions = map[string][]string{"Test_DimensionName1": {"Value1"}}
query.MetricQueryType = models.MetricQueryTypeSearch
query.MatchExact = false
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, shouldSkipFetchingWildcards)
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, shouldSkipFetchingWildcards)
assert.Nil(t, err)
assert.Len(t, queries, 1)
assert.NotNil(t, queries[0].Dimensions["Test_DimensionName1"], 1)
@ -119,7 +119,7 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
query.MetricName = "Test_MetricName1"
query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}}
query.MetricQueryType = models.MetricQueryTypeSearch
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, shouldSkipFetchingWildcards)
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, shouldSkipFetchingWildcards)
assert.Nil(t, err)
assert.Len(t, queries, 1)
assert.NotNil(t, queries[0].Dimensions["Test_DimensionName1"])
@ -132,14 +132,14 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}}
query.MetricQueryType = models.MetricQueryTypeSearch
query.MatchExact = false
api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{
{MetricName: utils.Pointer("Test_MetricName1"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value1")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Value2")}}},
{MetricName: utils.Pointer("Test_MetricName2"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value3")}}},
{MetricName: utils.Pointer("Test_MetricName3"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value4")}}},
{MetricName: utils.Pointer("Test_MetricName4"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value2")}}},
api := &mocks.MetricsAPI{Metrics: []cloudwatchtypes.Metric{
{MetricName: utils.Pointer("Test_MetricName1"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value1")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Value2")}}},
{MetricName: utils.Pointer("Test_MetricName2"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value3")}}},
{MetricName: utils.Pointer("Test_MetricName3"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value4")}}},
{MetricName: utils.Pointer("Test_MetricName4"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value2")}}},
}}
api.On("ListMetricsPagesWithContext").Return(nil)
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, shouldSkipFetchingWildcards)
api.On("ListMetrics").Return(nil)
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, shouldSkipFetchingWildcards)
assert.Nil(t, err)
assert.Len(t, queries, 1)
assert.Equal(t, map[string][]string{"Test_DimensionName1": {"Value1", "Value2", "Value3", "Value4"}}, queries[0].Dimensions)
@ -167,14 +167,14 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
}
query.MetricQueryType = models.MetricQueryTypeQuery
api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value1")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value1")}}},
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value2")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value2")}}},
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value3")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value3")}}},
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value4")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value4")}}},
api := &mocks.MetricsAPI{Metrics: []cloudwatchtypes.Metric{
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value1")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value1")}}},
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value2")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value2")}}},
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value3")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value3")}}},
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value4")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value4")}}},
}}
api.On("ListMetricsPagesWithContext").Return(nil)
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
api.On("ListMetrics").Return(nil)
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
assert.Nil(t, err)
assert.Len(t, queries, 1)
assert.Equal(t, map[string][]string{
@ -190,7 +190,7 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
query.Dimensions = map[string][]string{}
query.MetricQueryType = models.MetricQueryTypeQuery
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
assert.Nil(t, err)
assert.Len(t, queries, 1)
assert.Equal(t, map[string][]string{}, queries[0].Dimensions)

View File

@ -4,15 +4,16 @@ import (
"context"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
)
func (e *cloudWatchExecutor) executeRequest(ctx context.Context, client cloudwatchiface.CloudWatchAPI,
func (ds *DataSource) executeRequest(ctx context.Context, client models.CWClient,
metricDataInput *cloudwatch.GetMetricDataInput) ([]*cloudwatch.GetMetricDataOutput, error) {
mdo := make([]*cloudwatch.GetMetricDataOutput, 0)
@ -26,7 +27,7 @@ func (e *cloudWatchExecutor) executeRequest(ctx context.Context, client cloudwat
*metricDataInput.EndTime = metricDataInput.EndTime.Truncate(time.Minute).Add(time.Minute)
}
resp, err := client.GetMetricDataWithContext(ctx, metricDataInput)
resp, err := client.GetMetricData(ctx, metricDataInput)
if err != nil {
return mdo, backend.DownstreamError(err)
}

View File

@ -5,8 +5,10 @@ import (
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/stretchr/testify/assert"
@ -16,39 +18,39 @@ import (
func TestGetMetricDataExecutorTestRequest(t *testing.T) {
t.Run("Should round up end time if cloudWatchRoundUpEndTime is enabled", func(t *testing.T) {
executor := &cloudWatchExecutor{}
executor := &DataSource{}
queryEndTime, _ := time.Parse("2006-01-02T15:04:05Z07:00", "2024-05-01T01:45:04Z")
inputs := &cloudwatch.GetMetricDataInput{EndTime: &queryEndTime, MetricDataQueries: []*cloudwatch.MetricDataQuery{}}
inputs := &cloudwatch.GetMetricDataInput{EndTime: &queryEndTime, MetricDataQueries: []cloudwatchtypes.MetricDataQuery{}}
mockMetricClient := &mocks.MetricsAPI{}
mockMetricClient.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(
mockMetricClient.On("GetMetricData", mock.Anything, mock.Anything, mock.Anything).Return(
&cloudwatch.GetMetricDataOutput{
MetricDataResults: []*cloudwatch.MetricDataResult{{Values: []*float64{}}},
MetricDataResults: []cloudwatchtypes.MetricDataResult{{Values: []float64{}}},
}, nil).Once()
_, err := executor.executeRequest(contextWithFeaturesEnabled(features.FlagCloudWatchRoundUpEndTime), mockMetricClient, inputs)
require.NoError(t, err)
expectedTime, _ := time.Parse("2006-01-02T15:04:05Z07:00", "2024-05-01T01:46:00Z")
expectedInput := &cloudwatch.GetMetricDataInput{EndTime: &expectedTime, MetricDataQueries: []*cloudwatch.MetricDataQuery{}}
mockMetricClient.AssertCalled(t, "GetMetricDataWithContext", mock.Anything, expectedInput, mock.Anything)
expectedInput := &cloudwatch.GetMetricDataInput{EndTime: &expectedTime, MetricDataQueries: []cloudwatchtypes.MetricDataQuery{}}
mockMetricClient.AssertCalled(t, "GetMetricData", mock.Anything, expectedInput, mock.Anything)
})
}
func TestGetMetricDataExecutorTestResponse(t *testing.T) {
executor := &cloudWatchExecutor{}
inputs := &cloudwatch.GetMetricDataInput{EndTime: aws.Time(time.Now()), MetricDataQueries: []*cloudwatch.MetricDataQuery{}}
executor := &DataSource{}
inputs := &cloudwatch.GetMetricDataInput{EndTime: aws.Time(time.Now()), MetricDataQueries: []cloudwatchtypes.MetricDataQuery{}}
mockMetricClient := &mocks.MetricsAPI{}
mockMetricClient.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(
mockMetricClient.On("GetMetricData", mock.Anything, mock.Anything, mock.Anything).Return(
&cloudwatch.GetMetricDataOutput{
MetricDataResults: []*cloudwatch.MetricDataResult{{Values: []*float64{aws.Float64(12.3), aws.Float64(23.5)}}},
MetricDataResults: []cloudwatchtypes.MetricDataResult{{Values: []float64{12.3, 23.5}}},
NextToken: aws.String("next"),
}, nil).Once()
mockMetricClient.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(
mockMetricClient.On("GetMetricData", mock.Anything, mock.Anything, mock.Anything).Return(
&cloudwatch.GetMetricDataOutput{
MetricDataResults: []*cloudwatch.MetricDataResult{{Values: []*float64{aws.Float64(100)}}},
MetricDataResults: []cloudwatchtypes.MetricDataResult{{Values: []float64{100}}},
}, nil).Once()
res, err := executor.executeRequest(context.Background(), mockMetricClient, inputs)
require.NoError(t, err)
require.Len(t, res, 2)
require.Len(t, res[0].MetricDataResults[0].Values, 2)
assert.Equal(t, 23.5, *res[0].MetricDataResults[0].Values[1])
assert.Equal(t, 100.0, *res[1].MetricDataResults[0].Values[0])
assert.Equal(t, 23.5, res[0].MetricDataResults[0].Values[1])
assert.Equal(t, 100.0, res[1].MetricDataResults[0].Values[0])
}

View File

@ -10,11 +10,12 @@ import (
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
"github.com/aws/smithy-go"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"golang.org/x/sync/errgroup"
@ -25,10 +26,8 @@ import (
)
const (
limitExceededException = "LimitExceededException"
throttlingException = "ThrottlingException"
defaultEventLimit = int64(10)
defaultLogGroupLimit = int64(50)
defaultEventLimit = int32(10)
defaultLogGroupLimit = int32(50)
logIdentifierInternal = "__log__grafana_internal__"
logStreamIdentifierInternal = "__logstream__grafana_internal__"
)
@ -43,46 +42,7 @@ func (e *AWSError) Error() string {
return fmt.Sprintf("CloudWatch error: %s: %s", e.Code, e.Message)
}
// StartQueryInputWithLanguage copies the StartQueryInput struct from aws-sdk-go@v1.55.5
// (https://github.com/aws/aws-sdk-go/blob/7112c0a0c2d01713a9db2d57f0e5722225baf5b5/service/cloudwatchlogs/api.go#L19541)
// to add support for the new QueryLanguage parameter, which is unlikely to be backported
// since v1 of the aws-sdk-go is in maintenance mode. We've removed the comments for
// clarity.
type StartQueryInputWithLanguage struct {
_ struct{} `type:"structure"`
EndTime *int64 `locationName:"endTime" type:"long" required:"true"`
Limit *int64 `locationName:"limit" min:"1" type:"integer"`
LogGroupIdentifiers []*string `locationName:"logGroupIdentifiers" type:"list"`
LogGroupName *string `locationName:"logGroupName" min:"1" type:"string"`
LogGroupNames []*string `locationName:"logGroupNames" type:"list"`
QueryString *string `locationName:"queryString" type:"string" required:"true"`
// QueryLanguage is the only change here from the original code.
QueryLanguage *string `locationName:"queryLanguage" type:"string"`
StartTime *int64 `locationName:"startTime" type:"long" required:"true"`
}
type WithQueryLanguageFunc func(language *dataquery.LogsQueryLanguage) func(*request.Request)
// WithQueryLanguage assigns the function to a variable in order to mock it in log_actions_test.go
var WithQueryLanguage WithQueryLanguageFunc = withQueryLanguage
func withQueryLanguage(language *dataquery.LogsQueryLanguage) func(request *request.Request) {
return func(request *request.Request) {
sqi := request.Params.(*cloudwatchlogs.StartQueryInput)
request.Params = &StartQueryInputWithLanguage{
EndTime: sqi.EndTime,
Limit: sqi.Limit,
LogGroupIdentifiers: sqi.LogGroupIdentifiers,
LogGroupName: sqi.LogGroupName,
LogGroupNames: sqi.LogGroupNames,
QueryString: sqi.QueryString,
QueryLanguage: (*string)(language),
StartTime: sqi.StartTime,
}
}
}
func (e *cloudWatchExecutor) executeLogActions(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
func (ds *DataSource) executeLogActions(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
resp := backend.NewQueryDataResponse()
resultChan := make(chan backend.Responses, len(req.Queries))
@ -97,7 +57,7 @@ func (e *cloudWatchExecutor) executeLogActions(ctx context.Context, req *backend
query := query
eg.Go(func() error {
dataframe, err := e.executeLogAction(ectx, logsQuery, query, req.PluginContext)
dataframe, err := ds.executeLogAction(ectx, logsQuery, query)
if err != nil {
resultChan <- backend.Responses{
query.RefID: backend.ErrorResponseWithErrorSource(err),
@ -134,71 +94,64 @@ func (e *cloudWatchExecutor) executeLogActions(ctx context.Context, req *backend
return resp, nil
}
func (e *cloudWatchExecutor) executeLogAction(ctx context.Context, logsQuery models.LogsQuery, query backend.DataQuery, pluginCtx backend.PluginContext) (*data.Frame, error) {
instance, err := e.getInstance(ctx, pluginCtx)
if err != nil {
return nil, err
}
region := instance.Settings.Region
func (ds *DataSource) executeLogAction(ctx context.Context, logsQuery models.LogsQuery, query backend.DataQuery) (*data.Frame, error) {
region := ds.Settings.Region
if logsQuery.Region != "" {
region = logsQuery.Region
}
logsClient, err := e.getCWLogsClient(ctx, pluginCtx, region)
logsClient, err := ds.getCWLogsClient(ctx, region)
if err != nil {
return nil, err
}
var data *data.Frame = nil
var frame *data.Frame
switch logsQuery.Subtype {
case "StartQuery":
data, err = e.handleStartQuery(ctx, logsClient, logsQuery, query.TimeRange, query.RefID)
frame, err = ds.handleStartQuery(ctx, logsClient, logsQuery, query.TimeRange, query.RefID)
case "StopQuery":
data, err = e.handleStopQuery(ctx, logsClient, logsQuery)
frame, err = ds.handleStopQuery(ctx, logsClient, logsQuery)
case "GetQueryResults":
data, err = e.handleGetQueryResults(ctx, logsClient, logsQuery, query.RefID)
frame, err = ds.handleGetQueryResults(ctx, logsClient, logsQuery, query.RefID)
case "GetLogEvents":
data, err = e.handleGetLogEvents(ctx, logsClient, logsQuery)
frame, err = ds.handleGetLogEvents(ctx, logsClient, logsQuery)
}
if err != nil {
return nil, fmt.Errorf("failed to execute log action with subtype: %s: %w", logsQuery.Subtype, err)
}
return data, nil
return frame, nil
}
func (e *cloudWatchExecutor) handleGetLogEvents(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
func (ds *DataSource) handleGetLogEvents(ctx context.Context, logsClient models.CWLogsClient,
logsQuery models.LogsQuery) (*data.Frame, error) {
limit := defaultEventLimit
if logsQuery.Limit != nil && *logsQuery.Limit > 0 {
limit = *logsQuery.Limit
}
if logsQuery.LogGroupName == "" {
return nil, backend.DownstreamError(fmt.Errorf("parameter 'logGroupName' is required"))
}
if logsQuery.LogStreamName == "" {
return nil, backend.DownstreamError(fmt.Errorf("parameter 'logStreamName' is required"))
}
queryRequest := &cloudwatchlogs.GetLogEventsInput{
Limit: aws.Int64(limit),
Limit: aws.Int32(limit),
StartFromHead: aws.Bool(logsQuery.StartFromHead),
LogGroupName: &logsQuery.LogGroupName,
LogStreamName: &logsQuery.LogStreamName,
}
if logsQuery.LogGroupName == "" {
return nil, backend.DownstreamError(fmt.Errorf("Error: Parameter 'logGroupName' is required"))
}
queryRequest.SetLogGroupName(logsQuery.LogGroupName)
if logsQuery.LogStreamName == "" {
return nil, backend.DownstreamError(fmt.Errorf("Error: Parameter 'logStreamName' is required"))
}
queryRequest.SetLogStreamName(logsQuery.LogStreamName)
if logsQuery.StartTime != nil && *logsQuery.StartTime != 0 {
queryRequest.SetStartTime(*logsQuery.StartTime)
queryRequest.StartTime = logsQuery.StartTime
}
if logsQuery.EndTime != nil && *logsQuery.EndTime != 0 {
queryRequest.SetEndTime(*logsQuery.EndTime)
queryRequest.EndTime = logsQuery.EndTime
}
logEvents, err := logsClient.GetLogEventsWithContext(ctx, queryRequest)
logEvents, err := logsClient.GetLogEvents(ctx, queryRequest)
if err != nil {
return nil, backend.DownstreamError(err)
}
@ -223,7 +176,7 @@ func (e *cloudWatchExecutor) handleGetLogEvents(ctx context.Context, logsClient
return data.NewFrame("logEvents", timestampField, messageField), nil
}
func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
func (ds *DataSource) executeStartQuery(ctx context.Context, logsClient models.CWLogsClient,
logsQuery models.LogsQuery, timeRange backend.TimeRange) (*cloudwatchlogs.StartQueryOutput, error) {
startTime := timeRange.From
endTime := timeRange.To
@ -267,36 +220,36 @@ func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient c
// due to a bug in the startQuery api, we remove * from the arn, otherwise it throws an error
logGroupIdentifiers = append(logGroupIdentifiers, strings.TrimSuffix(arn, "*"))
}
startQueryInput.LogGroupIdentifiers = aws.StringSlice(logGroupIdentifiers)
startQueryInput.LogGroupIdentifiers = logGroupIdentifiers
} else {
// even though log group names are being phased out, we still need to support them for backwards compatibility and alert queries
startQueryInput.LogGroupNames = aws.StringSlice(logsQuery.LogGroupNames)
startQueryInput.LogGroupNames = logsQuery.LogGroupNames
}
}
if logsQuery.Limit != nil {
startQueryInput.Limit = aws.Int64(*logsQuery.Limit)
startQueryInput.Limit = aws.Int32(*logsQuery.Limit)
}
if logsQuery.QueryLanguage != nil {
startQueryInput.QueryLanguage = cloudwatchlogstypes.QueryLanguage(*logsQuery.QueryLanguage)
}
e.logger.FromContext(ctx).Debug("Calling startquery with context with input", "input", startQueryInput)
resp, err := logsClient.StartQueryWithContext(ctx, startQueryInput, WithQueryLanguage(logsQuery.QueryLanguage))
ds.logger.FromContext(ctx).Debug("Calling startquery with context with input", "input", startQueryInput)
resp, err := logsClient.StartQuery(ctx, startQueryInput)
if err != nil {
var awsErr awserr.Error
if errors.As(err, &awsErr) && awsErr.Code() == "LimitExceededException" {
e.logger.FromContext(ctx).Debug("ExecuteStartQuery limit exceeded", "err", awsErr)
err = &AWSError{Code: limitExceededException, Message: err.Error()}
} else if errors.As(err, &awsErr) && awsErr.Code() == "ThrottlingException" {
e.logger.FromContext(ctx).Debug("ExecuteStartQuery rate exceeded", "err", awsErr)
err = &AWSError{Code: throttlingException, Message: err.Error()}
if errors.Is(err, &cloudwatchlogstypes.LimitExceededException{}) {
ds.logger.FromContext(ctx).Debug("ExecuteStartQuery limit exceeded", "err", err)
} else if errors.Is(err, &cloudwatchlogstypes.ThrottlingException{}) {
ds.logger.FromContext(ctx).Debug("ExecuteStartQuery rate exceeded", "err", err)
}
err = backend.DownstreamError(err)
}
return resp, err
}
func (e *cloudWatchExecutor) handleStartQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
func (ds *DataSource) handleStartQuery(ctx context.Context, logsClient models.CWLogsClient,
logsQuery models.LogsQuery, timeRange backend.TimeRange, refID string) (*data.Frame, error) {
startQueryResponse, err := e.executeStartQuery(ctx, logsClient, logsQuery, timeRange)
startQueryResponse, err := ds.executeStartQuery(ctx, logsClient, logsQuery, timeRange)
if err != nil {
return nil, err
}
@ -318,20 +271,19 @@ func (e *cloudWatchExecutor) handleStartQuery(ctx context.Context, logsClient cl
return dataFrame, nil
}
func (e *cloudWatchExecutor) executeStopQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
func (ds *DataSource) executeStopQuery(ctx context.Context, logsClient models.CWLogsClient,
logsQuery models.LogsQuery) (*cloudwatchlogs.StopQueryOutput, error) {
queryInput := &cloudwatchlogs.StopQueryInput{
QueryId: aws.String(logsQuery.QueryId),
}
response, err := logsClient.StopQueryWithContext(ctx, queryInput)
response, err := logsClient.StopQuery(ctx, queryInput)
if err != nil {
// If the query has already stopped by the time CloudWatch receives the stop query request,
// an "InvalidParameterException" error is returned. For our purposes though the query has been
// stopped, so we ignore the error.
var awsErr awserr.Error
if errors.As(err, &awsErr) && awsErr.Code() == "InvalidParameterException" {
response = &cloudwatchlogs.StopQueryOutput{Success: aws.Bool(false)}
if errors.Is(err, &cloudwatchlogstypes.InvalidParameterException{}) {
response = &cloudwatchlogs.StopQueryOutput{Success: false}
err = nil
} else {
err = backend.DownstreamError(err)
@ -341,37 +293,37 @@ func (e *cloudWatchExecutor) executeStopQuery(ctx context.Context, logsClient cl
return response, err
}
func (e *cloudWatchExecutor) handleStopQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
func (ds *DataSource) handleStopQuery(ctx context.Context, logsClient models.CWLogsClient,
logsQuery models.LogsQuery) (*data.Frame, error) {
response, err := e.executeStopQuery(ctx, logsClient, logsQuery)
response, err := ds.executeStopQuery(ctx, logsClient, logsQuery)
if err != nil {
return nil, err
}
dataFrame := data.NewFrame("StopQueryResponse", data.NewField("success", nil, []bool{*response.Success}))
dataFrame := data.NewFrame("StopQueryResponse", data.NewField("success", nil, []bool{response.Success}))
return dataFrame, nil
}
func (e *cloudWatchExecutor) executeGetQueryResults(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
func (ds *DataSource) executeGetQueryResults(ctx context.Context, logsClient models.CWLogsClient,
logsQuery models.LogsQuery) (*cloudwatchlogs.GetQueryResultsOutput, error) {
queryInput := &cloudwatchlogs.GetQueryResultsInput{
QueryId: aws.String(logsQuery.QueryId),
}
getQueryResultsResponse, err := logsClient.GetQueryResultsWithContext(ctx, queryInput)
getQueryResultsResponse, err := logsClient.GetQueryResults(ctx, queryInput)
if err != nil {
var awsErr awserr.Error
var awsErr smithy.APIError
if errors.As(err, &awsErr) {
err = &AWSError{Code: awsErr.Code(), Message: err.Error()}
err = &AWSError{Code: awsErr.ErrorCode(), Message: awsErr.ErrorMessage()}
}
err = backend.DownstreamError(err)
}
return getQueryResultsResponse, err
}
func (e *cloudWatchExecutor) handleGetQueryResults(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
func (ds *DataSource) handleGetQueryResults(ctx context.Context, logsClient models.CWLogsClient,
logsQuery models.LogsQuery, refID string) (*data.Frame, error) {
getQueryResultsOutput, err := e.executeGetQueryResults(ctx, logsClient, logsQuery)
getQueryResultsOutput, err := ds.executeGetQueryResults(ctx, logsClient, logsQuery)
if err != nil {
return nil, err
}

View File

@ -6,19 +6,13 @@ import (
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/kinds/dataquery"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
@ -36,7 +30,7 @@ func TestQuery_handleGetLogEvents_passes_nil_start_and_end_times_to_GetLogEvents
var cli fakeCWLogsClient
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
NewCWLogsClient = func(cfg aws.Config) models.CWLogsClient {
return &cli
}
const refID = "A"
@ -57,7 +51,7 @@ func TestQuery_handleGetLogEvents_passes_nil_start_and_end_times_to_GetLogEvents
expectedInput: []*cloudwatchlogs.GetLogEventsInput{
{
EndTime: aws.Int64(1),
Limit: aws.Int64(10),
Limit: aws.Int32(10),
LogGroupName: aws.String("foo"),
LogStreamName: aws.String("bar"),
StartFromHead: aws.Bool(false),
@ -76,7 +70,7 @@ func TestQuery_handleGetLogEvents_passes_nil_start_and_end_times_to_GetLogEvents
expectedInput: []*cloudwatchlogs.GetLogEventsInput{
{
StartTime: aws.Int64(1),
Limit: aws.Int64(10),
Limit: aws.Int32(10),
LogGroupName: aws.String("foo"),
LogStreamName: aws.String("bar"),
StartFromHead: aws.Bool(true),
@ -88,13 +82,8 @@ func TestQuery_handleGetLogEvents_passes_nil_start_and_end_times_to_GetLogEvents
for name, test := range testCases {
t.Run(name, func(t *testing.T) {
cli = fakeCWLogsClient{}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
executor := newExecutor(im, log.NewNullLogger())
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
ds := newTestDatasource()
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
@ -108,8 +97,8 @@ func TestQuery_handleGetLogEvents_passes_nil_start_and_end_times_to_GetLogEvents
})
require.NoError(t, err)
require.Len(t, cli.calls.getEventsWithContext, 1)
assert.Equal(t, test.expectedInput, cli.calls.getEventsWithContext)
require.Len(t, cli.calls.getEvents, 1)
assert.Equal(t, test.expectedInput, cli.calls.getEvents)
})
}
}
@ -120,22 +109,19 @@ func TestQuery_GetLogEvents_returns_response_from_GetLogEvents_to_data_frame_fie
NewCWLogsClient = origNewCWLogsClient
})
var cli *mocks.MockLogEvents
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
NewCWLogsClient = func(cfg aws.Config) models.CWLogsClient {
return cli
}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
executor := newExecutor(im, log.NewNullLogger())
ds := newTestDatasource()
cli = &mocks.MockLogEvents{}
cli.On("GetLogEventsWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetLogEventsOutput{
Events: []*cloudwatchlogs.OutputLogEvent{{
cli.On("GetLogEvents", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetLogEventsOutput{
Events: []cloudwatchlogstypes.OutputLogEvent{{
Message: utils.Pointer("some message"),
Timestamp: utils.Pointer(int64(15)),
}}}, nil)
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
resp, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
@ -174,7 +160,7 @@ func TestQuery_StartQuery(t *testing.T) {
var cli fakeCWLogsClient
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
NewCWLogsClient = func(cfg aws.Config) models.CWLogsClient {
return &cli
}
@ -183,18 +169,18 @@ func TestQuery_StartQuery(t *testing.T) {
cli = fakeCWLogsClient{
logGroupFields: cloudwatchlogs.GetLogGroupFieldsOutput{
LogGroupFields: []*cloudwatchlogs.LogGroupField{
LogGroupFields: []cloudwatchlogstypes.LogGroupField{
{
Name: aws.String("field_a"),
Percent: aws.Int64(100),
Percent: 100,
},
{
Name: aws.String("field_b"),
Percent: aws.Int64(30),
Percent: 30,
},
{
Name: aws.String("field_c"),
Percent: aws.Int64(55),
Percent: 55,
},
},
},
@ -205,16 +191,10 @@ func TestQuery_StartQuery(t *testing.T) {
To: time.Unix(1584700643, 0),
}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{
AWSDatasourceSettings: awsds.AWSDatasourceSettings{
Region: "us-east-2",
},
}, sessions: &fakeSessionCache{}}, nil
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.Region = "us-east-2"
})
executor := newExecutor(im, log.NewNullLogger())
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
resp, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
@ -241,18 +221,18 @@ func TestQuery_StartQuery(t *testing.T) {
const refID = "A"
cli = fakeCWLogsClient{
logGroupFields: cloudwatchlogs.GetLogGroupFieldsOutput{
LogGroupFields: []*cloudwatchlogs.LogGroupField{
LogGroupFields: []cloudwatchlogstypes.LogGroupField{
{
Name: aws.String("field_a"),
Percent: aws.Int64(100),
Percent: 100,
},
{
Name: aws.String("field_b"),
Percent: aws.Int64(30),
Percent: 30,
},
{
Name: aws.String("field_c"),
Percent: aws.Int64(55),
Percent: 55,
},
},
},
@ -263,16 +243,10 @@ func TestQuery_StartQuery(t *testing.T) {
To: time.Unix(1584873443000, 0),
}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{
AWSDatasourceSettings: awsds.AWSDatasourceSettings{
Region: "us-east-2",
},
}, sessions: &fakeSessionCache{}}, nil
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.Region = "us-east-2"
})
executor := newExecutor(im, log.NewNullLogger())
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
resp, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
@ -311,26 +285,6 @@ func TestQuery_StartQuery(t *testing.T) {
})
}
type withQueryLanguageMock struct {
capturedLanguage *dataquery.LogsQueryLanguage
mockWithQueryLanguage func(language *dataquery.LogsQueryLanguage) func(request *request.Request)
}
func newWithQueryLanguageMock() *withQueryLanguageMock {
mock := &withQueryLanguageMock{
capturedLanguage: new(dataquery.LogsQueryLanguage),
}
mock.mockWithQueryLanguage = func(language *dataquery.LogsQueryLanguage) func(request *request.Request) {
*mock.capturedLanguage = *language
return func(req *request.Request) {
}
}
return mock
}
func Test_executeStartQuery(t *testing.T) {
origNewCWLogsClient := NewCWLogsClient
t.Cleanup(func() {
@ -339,15 +293,15 @@ func Test_executeStartQuery(t *testing.T) {
var cli fakeCWLogsClient
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
NewCWLogsClient = func(cfg aws.Config) models.CWLogsClient {
return &cli
}
t.Run("successfully parses information from JSON to StartQueryWithContext for language", func(t *testing.T) {
t.Run("successfully parses information from JSON to StartQuery for language", func(t *testing.T) {
testCases := map[string]struct {
queries []backend.DataQuery
expectedOutput []*cloudwatchlogs.StartQueryInput
queryLanguage dataquery.LogsQueryLanguage
queryLanguage cloudwatchlogstypes.QueryLanguage
}{
"not defined": {
queries: []backend.DataQuery{
@ -366,11 +320,12 @@ func Test_executeStartQuery(t *testing.T) {
expectedOutput: []*cloudwatchlogs.StartQueryInput{{
StartTime: aws.Int64(0),
EndTime: aws.Int64(1),
Limit: aws.Int64(12),
Limit: aws.Int32(12),
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
LogGroupNames: []*string{aws.String("some name"), aws.String("another name")},
LogGroupNames: []string{"some name", "another name"},
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
}},
queryLanguage: dataquery.LogsQueryLanguageCWLI,
queryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
},
"CWLI": {
queries: []backend.DataQuery{{
@ -389,12 +344,13 @@ func Test_executeStartQuery(t *testing.T) {
{
StartTime: aws.Int64(0),
EndTime: aws.Int64(1),
Limit: aws.Int64(12),
Limit: aws.Int32(12),
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
LogGroupNames: []*string{aws.String("some name"), aws.String("another name")},
LogGroupNames: []string{"some name", "another name"},
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
},
},
queryLanguage: dataquery.LogsQueryLanguageCWLI,
queryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
},
"PPL": {
queries: []backend.DataQuery{{
@ -413,12 +369,13 @@ func Test_executeStartQuery(t *testing.T) {
{
StartTime: aws.Int64(0),
EndTime: aws.Int64(1),
Limit: aws.Int64(12),
Limit: aws.Int32(12),
QueryString: aws.String("source logs | fields @message"),
LogGroupNames: []*string{aws.String("some name"), aws.String("another name")},
LogGroupNames: []string{"some name", "another name"},
QueryLanguage: cloudwatchlogstypes.QueryLanguagePpl,
},
},
queryLanguage: dataquery.LogsQueryLanguagePPL,
queryLanguage: cloudwatchlogstypes.QueryLanguagePpl,
},
"SQL": {
queries: []backend.DataQuery{
@ -439,49 +396,35 @@ func Test_executeStartQuery(t *testing.T) {
{
StartTime: aws.Int64(0),
EndTime: aws.Int64(1),
Limit: aws.Int64(12),
Limit: aws.Int32(12),
QueryString: aws.String("SELECT * FROM logs"),
LogGroupNames: nil,
QueryLanguage: cloudwatchlogstypes.QueryLanguageSql,
},
},
queryLanguage: dataquery.LogsQueryLanguageSQL,
queryLanguage: cloudwatchlogstypes.QueryLanguageSql,
},
}
for name, test := range testCases {
t.Run(name, func(t *testing.T) {
cli = fakeCWLogsClient{}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
executor := newExecutor(im, log.NewNullLogger())
languageMock := newWithQueryLanguageMock()
originalWithQueryLanguage := WithQueryLanguage
WithQueryLanguage = languageMock.mockWithQueryLanguage
defer func() {
WithQueryLanguage = originalWithQueryLanguage
}()
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
ds := newTestDatasource()
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: test.queries,
})
assert.NoError(t, err)
assert.Equal(t, test.expectedOutput, cli.calls.startQueryWithContext)
assert.Equal(t, &test.queryLanguage, languageMock.capturedLanguage)
assert.Equal(t, test.expectedOutput, cli.calls.startQuery)
})
}
})
t.Run("does not populate StartQueryInput.limit when no limit provided", func(t *testing.T) {
cli = fakeCWLogsClient{}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
executor := newExecutor(im, log.NewNullLogger())
ds := newTestDatasource()
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
{
@ -496,18 +439,15 @@ func Test_executeStartQuery(t *testing.T) {
})
assert.NoError(t, err)
require.Len(t, cli.calls.startQueryWithContext, 1)
assert.Nil(t, cli.calls.startQueryWithContext[0].Limit)
require.Len(t, cli.calls.startQuery, 1)
assert.Nil(t, cli.calls.startQuery[0].Limit)
})
t.Run("attaches logGroupIdentifiers if the crossAccount feature is enabled", func(t *testing.T) {
cli = fakeCWLogsClient{}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
executor := newExecutor(im, log.NewNullLogger())
ds := newTestDatasource()
_, err := executor.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
_, err := ds.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
{
@ -530,21 +470,19 @@ func Test_executeStartQuery(t *testing.T) {
{
StartTime: aws.Int64(0),
EndTime: aws.Int64(1),
Limit: aws.Int64(12),
Limit: aws.Int32(12),
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
LogGroupIdentifiers: []*string{aws.String("fakeARN")},
LogGroupIdentifiers: []string{"fakeARN"},
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
},
}, cli.calls.startQueryWithContext)
}, cli.calls.startQuery)
})
t.Run("attaches logGroupIdentifiers if the crossAccount feature is enabled and strips out trailing *", func(t *testing.T) {
cli = fakeCWLogsClient{}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
executor := newExecutor(im, log.NewNullLogger())
ds := newTestDatasource()
_, err := executor.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
_, err := ds.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
{
@ -566,20 +504,18 @@ func Test_executeStartQuery(t *testing.T) {
{
StartTime: aws.Int64(0),
EndTime: aws.Int64(1),
Limit: aws.Int64(12),
Limit: aws.Int32(12),
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
LogGroupIdentifiers: []*string{aws.String("*fake**ARN")},
LogGroupIdentifiers: []string{"*fake**ARN"},
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
},
}, cli.calls.startQueryWithContext)
}, cli.calls.startQuery)
})
t.Run("uses LogGroupNames if the cross account feature flag is not enabled, and log group names is present", func(t *testing.T) {
cli = fakeCWLogsClient{}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
executor := newExecutor(im, log.NewNullLogger())
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
ds := newTestDatasource()
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
{
@ -601,20 +537,18 @@ func Test_executeStartQuery(t *testing.T) {
{
StartTime: aws.Int64(0),
EndTime: aws.Int64(1),
Limit: aws.Int64(12),
Limit: aws.Int32(12),
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
LogGroupNames: []*string{aws.String("/log-group-name")},
LogGroupNames: []string{"/log-group-name"},
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
},
}, cli.calls.startQueryWithContext)
}, cli.calls.startQuery)
})
t.Run("ignores logGroups if feature flag is disabled even if logGroupNames is not present", func(t *testing.T) {
cli = fakeCWLogsClient{}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
executor := newExecutor(im, log.NewNullLogger())
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
ds := newTestDatasource()
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
{
@ -635,20 +569,18 @@ func Test_executeStartQuery(t *testing.T) {
{
StartTime: aws.Int64(0),
EndTime: aws.Int64(1),
Limit: aws.Int64(12),
Limit: aws.Int32(12),
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
LogGroupNames: []*string{},
LogGroupNames: nil,
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
},
}, cli.calls.startQueryWithContext)
}, cli.calls.startQuery)
})
t.Run("it always uses logGroups when feature flag is enabled and ignores log group names", func(t *testing.T) {
cli = fakeCWLogsClient{}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
executor := newExecutor(im, log.NewNullLogger())
_, err := executor.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
ds := newTestDatasource()
_, err := ds.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
{
@ -670,11 +602,12 @@ func Test_executeStartQuery(t *testing.T) {
{
StartTime: aws.Int64(0),
EndTime: aws.Int64(1),
Limit: aws.Int64(12),
Limit: aws.Int32(12),
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
LogGroupIdentifiers: []*string{aws.String("*fake**ARN")},
LogGroupIdentifiers: []string{"*fake**ARN"},
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
},
}, cli.calls.startQueryWithContext)
}, cli.calls.startQuery)
})
}
@ -686,40 +619,36 @@ func TestQuery_StopQuery(t *testing.T) {
var cli fakeCWLogsClient
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
NewCWLogsClient = func(aws.Config) models.CWLogsClient {
return &cli
}
cli = fakeCWLogsClient{
logGroupFields: cloudwatchlogs.GetLogGroupFieldsOutput{
LogGroupFields: []*cloudwatchlogs.LogGroupField{
LogGroupFields: []cloudwatchlogstypes.LogGroupField{
{
Name: aws.String("field_a"),
Percent: aws.Int64(100),
Percent: 100,
},
{
Name: aws.String("field_b"),
Percent: aws.Int64(30),
Percent: 30,
},
{
Name: aws.String("field_c"),
Percent: aws.Int64(55),
Percent: 55,
},
},
},
}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
timeRange := backend.TimeRange{
From: time.Unix(1584873443, 0),
To: time.Unix(1584700643, 0),
}
executor := newExecutor(im, log.NewNullLogger())
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
ds := newTestDatasource()
resp, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
@ -758,14 +687,14 @@ func TestQuery_GetQueryResults(t *testing.T) {
var cli fakeCWLogsClient
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
NewCWLogsClient = func(aws.Config) models.CWLogsClient {
return &cli
}
const refID = "A"
cli = fakeCWLogsClient{
queryResults: cloudwatchlogs.GetQueryResultsOutput{
Results: [][]*cloudwatchlogs.ResultField{
Results: [][]cloudwatchlogstypes.ResultField{
{
{
Field: aws.String("@timestamp"),
@ -795,21 +724,17 @@ func TestQuery_GetQueryResults(t *testing.T) {
},
},
},
Statistics: &cloudwatchlogs.QueryStatistics{
BytesScanned: aws.Float64(512),
RecordsMatched: aws.Float64(256),
RecordsScanned: aws.Float64(1024),
Statistics: &cloudwatchlogstypes.QueryStatistics{
BytesScanned: 512,
RecordsMatched: 256,
RecordsScanned: 1024,
},
Status: aws.String("Complete"),
Status: "Complete",
},
}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
executor := newExecutor(im, log.NewNullLogger())
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
ds := newTestDatasource()
resp, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},

View File

@ -1,29 +1,30 @@
package routes
package cloudwatch
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
)
func TestLogGroupFieldsRoute(t *testing.T) {
reqCtxFunc := func(_ context.Context, pluginCtx backend.PluginContext, region string) (reqCtx models.RequestContext, err error) {
return models.RequestContext{}, err
}
origLogGroupsService := services.NewLogGroupsService
t.Cleanup(func() {
services.NewLogGroupsService = origLogGroupsService
})
t.Run("returns 400 if an invalid LogGroupFieldsRequest is used", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", `/log-group-fields?region=us-east-2`, nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupFieldsHandler, logger, nil))
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupFieldsHandler))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Equal(t, `{"Message":"error in LogGroupFieldsHandler: you need to specify either logGroupName or logGroupArn","Error":"you need to specify either logGroupName or logGroupArn","StatusCode":400}`, rr.Body.String())
@ -31,14 +32,15 @@ func TestLogGroupFieldsRoute(t *testing.T) {
t.Run("returns 500 if GetLogGroupFields method fails", func(t *testing.T) {
mockLogsService := mocks.LogsService{}
mockLogsService.On("GetLogGroupFieldsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroupField]{}, fmt.Errorf("error from api"))
newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) {
return &mockLogsService, nil
mockLogsService.On("GetLogGroupFields", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroupField]{}, fmt.Errorf("error from api"))
services.NewLogGroupsService = func(_ models.CloudWatchLogsAPIProvider, _ bool) models.LogGroupsProvider {
return &mockLogsService
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-group-fields?region=us-east-2&logGroupName=test", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupFieldsHandler, logger, reqCtxFunc))
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupFieldsHandler))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
@ -47,7 +49,7 @@ func TestLogGroupFieldsRoute(t *testing.T) {
t.Run("returns valid json response if everything is ok", func(t *testing.T) {
mockLogsService := mocks.LogsService{}
mockLogsService.On("GetLogGroupFieldsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroupField]{
mockLogsService.On("GetLogGroupFields", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroupField]{
{
AccountId: new(string),
Value: resources.LogGroupField{
@ -63,13 +65,14 @@ func TestLogGroupFieldsRoute(t *testing.T) {
},
},
}, nil)
newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) {
return &mockLogsService, nil
services.NewLogGroupsService = func(_ models.CloudWatchLogsAPIProvider, _ bool) models.LogGroupsProvider {
return &mockLogsService
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-group-fields?region=us-east-2&logGroupName=test", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupFieldsHandler, logger, reqCtxFunc))
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupFieldsHandler))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)

View File

@ -0,0 +1,219 @@
package cloudwatch
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
)
func TestLogGroupsRoute(t *testing.T) {
origLogGroupsService := services.NewLogGroupsService
t.Cleanup(func() {
services.NewLogGroupsService = origLogGroupsService
})
var mockLogsService = mocks.LogsService{}
services.NewLogGroupsService = func(models.CloudWatchLogsAPIProvider, bool) models.LogGroupsProvider {
return &mockLogsService
}
t.Run("successfully returns 1 log group with account id", func(t *testing.T) {
mockLogsService = mocks.LogsService{}
mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{{
Value: resources.LogGroup{
Arn: "some arn",
Name: "some name",
},
AccountId: utils.Pointer("111"),
}}, nil)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups", nil)
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.JSONEq(t, `[{"value":{"name":"some name", "arn":"some arn"},"accountId":"111"}]`, rr.Body.String())
})
t.Run("successfully returns multiple log groups with account id", func(t *testing.T) {
mockLogsService = mocks.LogsService{}
mockLogsService.On("GetLogGroups", mock.Anything).Return(
[]resources.ResourceResponse[resources.LogGroup]{
{
Value: resources.LogGroup{
Arn: "arn 1",
Name: "name 1",
},
AccountId: utils.Pointer("111"),
}, {
Value: resources.LogGroup{
Arn: "arn 2",
Name: "name 2",
},
AccountId: utils.Pointer("222"),
},
}, nil)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups", nil)
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.JSONEq(t, `[
{
"value":{
"name":"name 1",
"arn":"arn 1"
},
"accountId":"111"
},
{
"value":{
"name":"name 2",
"arn":"arn 2"
},
"accountId":"222"
}
]`, rr.Body.String())
})
t.Run("returns error when both logGroupPrefix and logGroup Pattern are provided", func(t *testing.T) {
mockLogsService = mocks.LogsService{}
mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups?logGroupNamePrefix=some-prefix&logGroupPattern=some-pattern", nil)
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.JSONEq(t, `{"Error":"cannot set both log group name prefix and pattern", "Message":"cannot set both log group name prefix and pattern: cannot set both log group name prefix and pattern", "StatusCode":400}`, rr.Body.String())
})
t.Run("passes default log group limit and nil for logGroupNamePrefix, accountId, and logGroupPattern", func(t *testing.T) {
mockLogsService = mocks.LogsService{}
mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups", nil)
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
handler.ServeHTTP(rr, req)
mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{
Limit: 50,
ResourceRequest: resources.ResourceRequest{},
LogGroupNamePrefix: nil,
LogGroupNamePattern: nil,
})
})
t.Run("passes default log group limit and nil for logGroupNamePrefix when both are absent", func(t *testing.T) {
mockLogsService = mocks.LogsService{}
mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups", nil)
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
handler.ServeHTTP(rr, req)
mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{
Limit: 50,
LogGroupNamePrefix: nil,
})
})
t.Run("passes log group limit from query parameter", func(t *testing.T) {
mockLogsService = mocks.LogsService{}
mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups?limit=2", nil)
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
handler.ServeHTTP(rr, req)
mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{
Limit: 2,
})
})
t.Run("passes logGroupPrefix from query parameter", func(t *testing.T) {
mockLogsService = mocks.LogsService{}
mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups?logGroupNamePrefix=some-prefix", nil)
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
handler.ServeHTTP(rr, req)
mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{
Limit: 50,
LogGroupNamePrefix: utils.Pointer("some-prefix"),
})
})
t.Run("passes logGroupPattern from query parameter", func(t *testing.T) {
mockLogsService = mocks.LogsService{}
mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups?logGroupPattern=some-pattern", nil)
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
handler.ServeHTTP(rr, req)
mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{
Limit: 50,
LogGroupNamePattern: utils.Pointer("some-pattern"),
})
})
t.Run("passes logGroupPattern from query parameter", func(t *testing.T) {
mockLogsService = mocks.LogsService{}
mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups?accountId=some-account-id", nil)
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
handler.ServeHTTP(rr, req)
mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{
Limit: 50,
ResourceRequest: resources.ResourceRequest{AccountId: utils.Pointer("some-account-id")},
})
})
t.Run("returns error if service returns error", func(t *testing.T) {
mockLogsService = mocks.LogsService{}
mockLogsService.On("GetLogGroups", mock.Anything).
Return([]resources.ResourceResponse[resources.LogGroup]{}, fmt.Errorf("some error"))
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups", nil)
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
assert.JSONEq(t, `{"Error":"some error","Message":"GetLogGroups error: some error","StatusCode":500}`, rr.Body.String())
})
}

View File

@ -7,7 +7,9 @@ import (
"strconv"
"time"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
@ -18,7 +20,7 @@ func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput, gro
return nil, fmt.Errorf("response is nil, cannot convert log results to data frames")
}
nonEmptyRows := make([][]*cloudwatchlogs.ResultField, 0)
nonEmptyRows := make([][]cloudwatchlogstypes.ResultField, 0)
for _, row := range response.Results {
// Sometimes CloudWatch can send empty rows
if len(row) == 0 {
@ -116,26 +118,20 @@ func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput, gro
queryStats := make([]data.QueryStat, 0)
if response.Statistics != nil {
if response.Statistics.BytesScanned != nil {
queryStats = append(queryStats, data.QueryStat{
FieldConfig: data.FieldConfig{DisplayName: "Bytes scanned"},
Value: *response.Statistics.BytesScanned,
})
}
queryStats = append(queryStats, data.QueryStat{
FieldConfig: data.FieldConfig{DisplayName: "Bytes scanned"},
Value: response.Statistics.BytesScanned,
})
if response.Statistics.RecordsScanned != nil {
queryStats = append(queryStats, data.QueryStat{
FieldConfig: data.FieldConfig{DisplayName: "Records scanned"},
Value: *response.Statistics.RecordsScanned,
})
}
queryStats = append(queryStats, data.QueryStat{
FieldConfig: data.FieldConfig{DisplayName: "Records scanned"},
Value: response.Statistics.RecordsScanned,
})
if response.Statistics.RecordsMatched != nil {
queryStats = append(queryStats, data.QueryStat{
FieldConfig: data.FieldConfig{DisplayName: "Records matched"},
Value: *response.Statistics.RecordsMatched,
})
}
queryStats = append(queryStats, data.QueryStat{
FieldConfig: data.FieldConfig{DisplayName: "Records matched"},
Value: response.Statistics.RecordsMatched,
})
}
frame := data.NewFrame("CloudWatchLogsResponse", newFields...)
@ -148,10 +144,8 @@ func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput, gro
frame.Meta.Stats = queryStats
}
if response.Status != nil {
frame.Meta.Custom = map[string]any{
"Status": *response.Status,
}
frame.Meta.Custom = map[string]any{
"Status": string(response.Status),
}
// Results aren't guaranteed to come ordered by time (ascending), so we need to sort
@ -159,7 +153,7 @@ func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput, gro
return frame, nil
}
func changeToStringField(lengthOfValues int, rows [][]*cloudwatchlogs.ResultField, logEventField string) []*string {
func changeToStringField(lengthOfValues int, rows [][]cloudwatchlogstypes.ResultField, logEventField string) []*string {
fieldValuesAsStrings := make([]*string, lengthOfValues)
for i, resultFields := range rows {
for _, field := range resultFields {

View File

@ -5,8 +5,10 @@ import (
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/stretchr/testify/assert"
@ -19,63 +21,63 @@ import (
func TestLogsResultsToDataframes(t *testing.T) {
fakeCloudwatchResponse := &cloudwatchlogs.GetQueryResultsOutput{
Results: [][]*cloudwatchlogs.ResultField{
Results: [][]cloudwatchlogstypes.ResultField{
{
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("@ptr"),
Value: aws.String("fake ptr"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("@timestamp"),
Value: aws.String("2020-03-02 15:04:05.000"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("line"),
Value: aws.String("test message 1"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("@logStream"),
Value: aws.String("fakelogstream"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("@log"),
Value: aws.String("fakelog"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String(logStreamIdentifierInternal),
Value: aws.String("fakelogstream"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String(logIdentifierInternal),
Value: aws.String("fakelog"),
},
},
{
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("@ptr"),
Value: aws.String("fake ptr"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("@timestamp"),
Value: aws.String("2020-03-02 16:04:05.000"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("line"),
Value: aws.String("test message 2"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("@logStream"),
Value: aws.String("fakelogstream"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("@log"),
Value: aws.String("fakelog"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String(logStreamIdentifierInternal),
Value: aws.String("fakelogstream"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String(logIdentifierInternal),
Value: aws.String("fakelog"),
},
@ -84,47 +86,47 @@ func TestLogsResultsToDataframes(t *testing.T) {
{},
// or rows with only timestamp
{
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("@timestamp"),
Value: aws.String("2020-03-02 17:04:05.000"),
},
},
{
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("@ptr"),
Value: aws.String("fake ptr"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("@timestamp"),
Value: aws.String("2020-03-02 17:04:05.000"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("line"),
Value: aws.String("test message 3"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("@logStream"),
Value: aws.String("fakelogstream"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("@log"),
Value: aws.String("fakelog"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String(logStreamIdentifierInternal),
Value: aws.String("fakelogstream"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String(logIdentifierInternal),
Value: aws.String("fakelog"),
},
},
},
Status: aws.String("ok"),
Statistics: &cloudwatchlogs.QueryStatistics{
BytesScanned: aws.Float64(2000),
RecordsMatched: aws.Float64(3),
RecordsScanned: aws.Float64(5000),
Status: "ok",
Statistics: &cloudwatchlogstypes.QueryStatistics{
BytesScanned: 2000,
RecordsMatched: 3,
RecordsScanned: 5000,
},
}
@ -224,33 +226,33 @@ func TestLogsResultsToDataframes(t *testing.T) {
func TestLogsResultsToDataframes_MixedTypes_NumericValuesMixedWithStringFallBackToStringValues(t *testing.T) {
dataframes, err := logsResultsToDataframes(&cloudwatchlogs.GetQueryResultsOutput{
Results: [][]*cloudwatchlogs.ResultField{
Results: [][]cloudwatchlogstypes.ResultField{
{
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("numberOrString"),
Value: aws.String("-1.234"),
},
},
{
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("numberOrString"),
Value: aws.String("1"),
},
},
{
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("numberOrString"),
Value: aws.String("not a number"),
},
},
{
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("numberOrString"),
Value: aws.String("2.000"),
},
},
},
Status: aws.String("ok"),
Status: "ok",
}, []string{})
require.NoError(t, err)
@ -284,27 +286,27 @@ func TestLogsResultsToDataframes_With_Millisecond_Timestamps(t *testing.T) {
ingestionTimeField := int64(1732790372916)
dataframes, err := logsResultsToDataframes(&cloudwatchlogs.GetQueryResultsOutput{
Results: [][]*cloudwatchlogs.ResultField{
Results: [][]cloudwatchlogstypes.ResultField{
{
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("@timestamp"),
Value: aws.String(fmt.Sprintf("%d", timestampField)),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("@ingestionTime"),
Value: aws.String(fmt.Sprintf("%d", ingestionTimeField)),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("stringTimeField"),
Value: aws.String(stringTimeField),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("message"),
Value: aws.String("log message"),
},
},
},
Status: aws.String("ok"),
Status: "ok",
}, []string{})
require.NoError(t, err)
@ -348,23 +350,23 @@ func TestLogsResultsToDataframes_With_Int_Grouping_Field(t *testing.T) {
timestampField := int64(1732749534876)
dataframes, err := logsResultsToDataframes(&cloudwatchlogs.GetQueryResultsOutput{
Results: [][]*cloudwatchlogs.ResultField{
Results: [][]cloudwatchlogstypes.ResultField{
{
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("@timestamp"),
Value: aws.String(fmt.Sprintf("%d", timestampField)),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("numberField"),
Value: aws.String("8"),
},
&cloudwatchlogs.ResultField{
cloudwatchlogstypes.ResultField{
Field: aws.String("groupingNumber"),
Value: aws.String("100"),
},
},
},
Status: aws.String("ok"),
Status: "ok",
}, []string{"groupingNumber"})
require.NoError(t, err)

View File

@ -7,8 +7,8 @@ import (
"fmt"
"time"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/kinds/dataquery"
@ -17,15 +17,9 @@ import (
const initialAlertPollPeriod = time.Second
var executeSyncLogQuery = func(ctx context.Context, e *cloudWatchExecutor, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
var executeSyncLogQuery = func(ctx context.Context, ds *DataSource, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
resp := backend.NewQueryDataResponse()
instance, err := e.getInstance(ctx, req.PluginContext)
if err != nil {
resp.Responses[req.Queries[0].RefID] = backend.ErrorResponseWithErrorSource(err)
return resp, nil
}
for _, q := range req.Queries {
var logsQuery models.LogsQuery
err := json.Unmarshal(q.JSON, &logsQuery)
@ -40,10 +34,10 @@ var executeSyncLogQuery = func(ctx context.Context, e *cloudWatchExecutor, req *
region := logsQuery.Region
if region == "" || region == defaultRegion {
logsQuery.Region = instance.Settings.Region
logsQuery.Region = ds.Settings.Region
}
logsClient, err := e.getCWLogsClient(ctx, req.PluginContext, region)
logsClient, err := ds.getCWLogsClient(ctx, region)
if err != nil {
return nil, err
}
@ -53,7 +47,7 @@ var executeSyncLogQuery = func(ctx context.Context, e *cloudWatchExecutor, req *
refId = q.RefID
}
getQueryResultsOutput, err := e.syncQuery(ctx, logsClient, q, logsQuery, instance.Settings.LogsTimeout.Duration)
getQueryResultsOutput, err := ds.syncQuery(ctx, logsClient, q, logsQuery, ds.Settings.LogsTimeout.Duration)
var sourceError backend.ErrorWithSource
if errors.As(err, &sourceError) {
resp.Responses[refId] = backend.ErrorResponseWithErrorSource(sourceError)
@ -86,9 +80,9 @@ var executeSyncLogQuery = func(ctx context.Context, e *cloudWatchExecutor, req *
return resp, nil
}
func (e *cloudWatchExecutor) syncQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
func (ds *DataSource) syncQuery(ctx context.Context, logsClient models.CWLogsClient,
queryContext backend.DataQuery, logsQuery models.LogsQuery, logsTimeout time.Duration) (*cloudwatchlogs.GetQueryResultsOutput, error) {
startQueryOutput, err := e.executeStartQuery(ctx, logsClient, logsQuery, queryContext.TimeRange)
startQueryOutput, err := ds.executeStartQuery(ctx, logsClient, logsQuery, queryContext.TimeRange)
if err != nil {
return nil, err
}
@ -113,11 +107,11 @@ func (e *cloudWatchExecutor) syncQuery(ctx context.Context, logsClient cloudwatc
attemptCount := 1
for range ticker.C {
res, err := e.executeGetQueryResults(ctx, logsClient, requestParams)
res, err := ds.executeGetQueryResults(ctx, logsClient, requestParams)
if err != nil {
return nil, err
}
if isTerminated(*res.Status) {
if isTerminated(res.Status) {
return res, err
}
if time.Duration(attemptCount)*time.Second >= logsTimeout {

View File

@ -7,15 +7,11 @@ import (
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
@ -31,19 +27,15 @@ func Test_executeSyncLogQuery(t *testing.T) {
})
var cli fakeCWLogsClient
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
NewCWLogsClient = func(aws.Config) models.CWLogsClient {
return &cli
}
t.Run("getCWLogsClient is called with region from input JSON", func(t *testing.T) {
cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}}
sess := fakeSessionCache{}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &sess}, nil
})
executor := newExecutor(im, log.NewNullLogger())
cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: "Complete"}}
ds := newTestDatasource()
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
Headers: map[string]string{headerFromAlert: "some value"},
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
@ -58,18 +50,15 @@ func Test_executeSyncLogQuery(t *testing.T) {
})
assert.NoError(t, err)
assert.Equal(t, []string{"some region"}, sess.calledRegions)
//assert.Equal(t, []string{"some region"}, sess.calledRegions)
})
t.Run("getCWLogsClient is called with region from instance manager when region is default", func(t *testing.T) {
cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}}
sess := fakeSessionCache{}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "instance manager's region"}}, sessions: &sess}, nil
cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: "Complete"}}
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.Region = "instance manager's region"
})
executor := newExecutor(im, log.NewNullLogger())
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
Headers: map[string]string{headerFromAlert: "some value"},
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
@ -84,7 +73,7 @@ func Test_executeSyncLogQuery(t *testing.T) {
})
assert.NoError(t, err)
assert.Equal(t, []string{"instance manager's region"}, sess.calledRegions)
//assert.Equal(t, []string{"instance manager's region"}, sess.calledRegions)
})
t.Run("with header", func(t *testing.T) {
@ -111,7 +100,7 @@ func Test_executeSyncLogQuery(t *testing.T) {
}
origExecuteSyncLogQuery := executeSyncLogQuery
var syncCalled bool
executeSyncLogQuery = func(ctx context.Context, e *cloudWatchExecutor, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
executeSyncLogQuery = func(ctx context.Context, e *DataSource, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
syncCalled = true
return nil, nil
}
@ -119,13 +108,11 @@ func Test_executeSyncLogQuery(t *testing.T) {
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
syncCalled = false
cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "instance manager's region"}}, sessions: &fakeSessionCache{}}, nil
cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: "Complete"}}
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.Region = "instance manager's region"
})
executor := newExecutor(im, log.NewNullLogger())
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
Headers: tc.headers,
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
@ -153,7 +140,7 @@ func Test_executeSyncLogQuery(t *testing.T) {
t.Run("when query mode is 'Logs' and does not include type or subtype", func(t *testing.T) {
origExecuteSyncLogQuery := executeSyncLogQuery
syncCalled := false
executeSyncLogQuery = func(ctx context.Context, e *cloudWatchExecutor, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
executeSyncLogQuery = func(ctx context.Context, e *DataSource, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
syncCalled = true
return nil, nil
}
@ -161,13 +148,12 @@ func Test_executeSyncLogQuery(t *testing.T) {
executeSyncLogQuery = origExecuteSyncLogQuery
})
cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "instance manager's region"}}, sessions: &fakeSessionCache{}}, nil
cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: "Complete"}}
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.Region = "instance manager's region"
})
executor := newExecutor(im, log.NewNullLogger())
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
{
@ -192,22 +178,19 @@ func Test_executeSyncLogQuery_handles_RefId_from_input_queries(t *testing.T) {
})
var cli *mockLogsSyncClient
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
NewCWLogsClient = func(aws.Config) models.CWLogsClient {
return cli
}
t.Run("when a query refId is not provided, 'A' is assigned by default", func(t *testing.T) {
cli = &mockLogsSyncClient{}
cli.On("StartQueryWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
cli.On("StartQuery", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
QueryId: aws.String("abcd-efgh-ijkl-mnop"),
}, nil)
cli.On("GetQueryResultsWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}, nil)
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
executor := newExecutor(im, log.NewNullLogger())
cli.On("GetQueryResults", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{Status: "Complete"}, nil)
ds := newTestDatasource()
res, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
res, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
Headers: map[string]string{headerFromAlert: "some value"},
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
@ -227,16 +210,13 @@ func Test_executeSyncLogQuery_handles_RefId_from_input_queries(t *testing.T) {
t.Run("when a query refId is provided, it is returned in the response", func(t *testing.T) {
cli = &mockLogsSyncClient{}
cli.On("StartQueryWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
cli.On("StartQuery", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
QueryId: aws.String("abcd-efgh-ijkl-mnop"),
}, nil)
cli.On("GetQueryResultsWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}, nil)
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
executor := newExecutor(im, log.NewNullLogger())
cli.On("GetQueryResults", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{Status: "Complete"}, nil)
ds := newTestDatasource()
res, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
res, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
Headers: map[string]string{headerFromAlert: "some value"},
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
@ -269,43 +249,40 @@ func Test_executeSyncLogQuery_handles_RefId_from_input_queries(t *testing.T) {
// when each query has a different response from AWS API calls, the RefIds are correctly reassigned to the associated response.
cli = &mockLogsSyncClient{}
// mock.MatchedBy makes sure that the QueryId below will only be returned when the input expression = "query string for A"
cli.On("StartQueryWithContext", mock.Anything, mock.MatchedBy(func(input *cloudwatchlogs.StartQueryInput) bool {
cli.On("StartQuery", mock.Anything, mock.MatchedBy(func(input *cloudwatchlogs.StartQueryInput) bool {
return *input.QueryString == "fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|query string for A"
}), mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
QueryId: aws.String("queryId for A"),
}, nil)
// mock.MatchedBy makes sure that the QueryId below will only be returned when the input expression = "query string for B"
cli.On("StartQueryWithContext", mock.Anything, mock.MatchedBy(func(input *cloudwatchlogs.StartQueryInput) bool {
cli.On("StartQuery", mock.Anything, mock.MatchedBy(func(input *cloudwatchlogs.StartQueryInput) bool {
return *input.QueryString == "fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|query string for B"
}), mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
QueryId: aws.String("queryId for B"),
}, nil)
cli.On("GetQueryResultsWithContext", mock.Anything, mock.MatchedBy(func(input *cloudwatchlogs.GetQueryResultsInput) bool {
cli.On("GetQueryResults", mock.Anything, mock.MatchedBy(func(input *cloudwatchlogs.GetQueryResultsInput) bool {
return *input.QueryId == "queryId for A"
}), mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{
// this result will only be returned when the argument is QueryId = "queryId for A"
Results: [][]*cloudwatchlogs.ResultField{{{
Results: [][]cloudwatchlogstypes.ResultField{{{
Field: utils.Pointer("@log"),
Value: utils.Pointer("A result"),
}}},
Status: aws.String("Complete")}, nil)
cli.On("GetQueryResultsWithContext", mock.Anything, mock.MatchedBy(func(input *cloudwatchlogs.GetQueryResultsInput) bool {
Status: "Complete"}, nil)
cli.On("GetQueryResults", mock.Anything, mock.MatchedBy(func(input *cloudwatchlogs.GetQueryResultsInput) bool {
return *input.QueryId == "queryId for B"
}), mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{
// this result will only be returned when the argument is QueryId = "queryId for B"
Results: [][]*cloudwatchlogs.ResultField{{{
Results: [][]cloudwatchlogstypes.ResultField{{{
Field: utils.Pointer("@log"),
Value: utils.Pointer("B result"),
}}},
Status: aws.String("Complete")}, nil)
Status: "Complete"}, nil)
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
executor := newExecutor(im, log.NewNullLogger())
ds := newTestDatasource()
res, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
res, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
Headers: map[string]string{headerFromAlert: "some value"},
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
@ -328,30 +305,29 @@ func Test_executeSyncLogQuery_handles_RefId_from_input_queries(t *testing.T) {
},
})
expectedLogFieldFromFirstCall := data.NewField("@log", nil, []*string{utils.Pointer("A result")}) // verifies the response from GetQueryResultsWithContext matches the input RefId A
expectedLogFieldFromFirstCall := data.NewField("@log", nil, []*string{utils.Pointer("A result")}) // verifies the response from GetQueryResults matches the input RefId A
assert.NoError(t, err)
respA, ok := res.Responses["A"]
require.True(t, ok)
assert.Equal(t, []*data.Field{expectedLogFieldFromFirstCall}, respA.Frames[0].Fields)
expectedLogFieldFromSecondCall := data.NewField("@log", nil, []*string{utils.Pointer("B result")}) // verifies the response from GetQueryResultsWithContext matches the input RefId B
expectedLogFieldFromSecondCall := data.NewField("@log", nil, []*string{utils.Pointer("B result")}) // verifies the response from GetQueryResults matches the input RefId B
respB, ok := res.Responses["B"]
require.True(t, ok)
assert.Equal(t, []*data.Field{expectedLogFieldFromSecondCall}, respB.Frames[0].Fields)
})
t.Run("when logsTimeout setting is defined, the polling period will be set to that variable", func(t *testing.T) {
cli = &mockLogsSyncClient{}
cli.On("StartQueryWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
cli.On("StartQuery", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
QueryId: aws.String("abcd-efgh-ijkl-mnop"),
}, nil)
cli.On("GetQueryResultsWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Running")}, nil)
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{LogsTimeout: models.Duration{Duration: time.Millisecond}}, sessions: &fakeSessionCache{}}, nil
cli.On("GetQueryResults", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{Status: "Running"}, nil)
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.LogsTimeout = models.Duration{Duration: time.Millisecond}
})
executor := newExecutor(im, log.NewNullLogger())
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
Headers: map[string]string{headerFromAlert: "some value"},
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
@ -366,24 +342,21 @@ func Test_executeSyncLogQuery_handles_RefId_from_input_queries(t *testing.T) {
},
})
assert.Error(t, err)
cli.AssertNumberOfCalls(t, "GetQueryResultsWithContext", 1)
cli.AssertNumberOfCalls(t, "GetQueryResults", 1)
})
t.Run("when getQueryResults returns aws error is returned, it keeps the context", func(t *testing.T) {
cli = &mockLogsSyncClient{}
cli.On("StartQueryWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
cli.On("StartQuery", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
QueryId: aws.String("abcd-efgh-ijkl-mnop"),
}, nil)
cli.On("GetQueryResultsWithContext", mock.Anything, mock.Anything, mock.Anything).Return(
&cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")},
&fakeAWSError{code: "foo", message: "bar"},
cli.On("GetQueryResults", mock.Anything, mock.Anything, mock.Anything).Return(
&cloudwatchlogs.GetQueryResultsOutput{Status: "Complete"},
&fakeSmithyError{code: "foo", message: "bar"},
)
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
executor := newExecutor(im, log.NewNullLogger())
ds := newTestDatasource()
res, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
res, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
Headers: map[string]string{headerFromAlert: "some value"},
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{

View File

@ -4,30 +4,31 @@ import (
"context"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
)
func (e *cloudWatchExecutor) buildMetricDataInput(ctx context.Context, startTime time.Time, endTime time.Time,
func (ds *DataSource) buildMetricDataInput(ctx context.Context, startTime time.Time, endTime time.Time,
queries []*models.CloudWatchQuery) (*cloudwatch.GetMetricDataInput, error) {
metricDataInput := &cloudwatch.GetMetricDataInput{
StartTime: aws.Time(startTime),
EndTime: aws.Time(endTime),
ScanBy: aws.String("TimestampAscending"),
ScanBy: cloudwatchtypes.ScanByTimestampAscending,
}
shouldSetLabelOptions := len(queries) > 0 && len(queries[0].TimezoneUTCOffset) > 0
if shouldSetLabelOptions {
metricDataInput.LabelOptions = &cloudwatch.LabelOptions{
metricDataInput.LabelOptions = &cloudwatchtypes.LabelOptions{
Timezone: aws.String(queries[0].TimezoneUTCOffset),
}
}
for _, query := range queries {
metricDataQuery, err := e.buildMetricDataQuery(ctx, query)
metricDataQuery, err := ds.buildMetricDataQuery(ctx, query)
if err != nil {
return nil, &models.QueryError{Err: err, RefID: query.RefId}
}

View File

@ -5,12 +5,12 @@ import (
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/aws"
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
)
@ -20,21 +20,21 @@ func TestMetricDataInputBuilder(t *testing.T) {
tests := []struct {
name string
timezoneUTCOffset string
expectedLabelOptions *cloudwatch.LabelOptions
expectedLabelOptions *cloudwatchtypes.LabelOptions
}{
{name: "when timezoneUTCOffset is provided", timezoneUTCOffset: "+1234", expectedLabelOptions: &cloudwatch.LabelOptions{Timezone: aws.String("+1234")}},
{name: "when timezoneUTCOffset is provided", timezoneUTCOffset: "+1234", expectedLabelOptions: &cloudwatchtypes.LabelOptions{Timezone: aws.String("+1234")}},
{name: "when timezoneUTCOffset is not provided", timezoneUTCOffset: "", expectedLabelOptions: nil},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
executor := newExecutor(nil, log.NewNullLogger())
ds := newTestDatasource()
query := getBaseQuery()
query.TimezoneUTCOffset = tc.timezoneUTCOffset
from := now.Add(time.Hour * -2)
to := now.Add(time.Hour * -1)
mdi, err := executor.buildMetricDataInput(context.Background(), from, to, []*models.CloudWatchQuery{query})
mdi, err := ds.buildMetricDataInput(context.Background(), from, to, []*models.CloudWatchQuery{query})
assert.NoError(t, err)
require.NotNil(t, mdi)

View File

@ -4,11 +4,10 @@ import (
"context"
"fmt"
"sort"
"strconv"
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/aws"
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
@ -16,8 +15,8 @@ import (
const keySeparator = "|&|"
func (e *cloudWatchExecutor) buildMetricDataQuery(ctx context.Context, query *models.CloudWatchQuery) (*cloudwatch.MetricDataQuery, error) {
mdq := &cloudwatch.MetricDataQuery{
func (ds *DataSource) buildMetricDataQuery(ctx context.Context, query *models.CloudWatchQuery) (cloudwatchtypes.MetricDataQuery, error) {
mdq := cloudwatchtypes.MetricDataQuery{
Id: aws.String(query.Id),
ReturnData: aws.Bool(query.ReturnData),
}
@ -28,10 +27,10 @@ func (e *cloudWatchExecutor) buildMetricDataQuery(ctx context.Context, query *mo
switch query.GetGetMetricDataAPIMode() {
case models.GMDApiModeMathExpression:
mdq.Period = aws.Int64(int64(query.Period))
mdq.Period = &query.Period
mdq.Expression = aws.String(query.Expression)
case models.GMDApiModeSQLExpression:
mdq.Period = aws.Int64(int64(query.Period))
mdq.Period = &query.Period
mdq.Expression = aws.String(query.SqlExpression)
case models.GMDApiModeInferredSearchExpression:
mdq.Expression = aws.String(buildSearchExpression(query, query.Statistic))
@ -39,17 +38,17 @@ func (e *cloudWatchExecutor) buildMetricDataQuery(ctx context.Context, query *mo
mdq.Label = aws.String(buildSearchExpressionLabel(query))
}
case models.GMDApiModeMetricStat:
mdq.MetricStat = &cloudwatch.MetricStat{
Metric: &cloudwatch.Metric{
mdq.MetricStat = &cloudwatchtypes.MetricStat{
Metric: &cloudwatchtypes.Metric{
Namespace: aws.String(query.Namespace),
MetricName: aws.String(query.MetricName),
Dimensions: make([]*cloudwatch.Dimension, 0),
Dimensions: make([]cloudwatchtypes.Dimension, 0),
},
Period: aws.Int64(int64(query.Period)),
Period: &query.Period,
}
for key, values := range query.Dimensions {
mdq.MetricStat.Metric.Dimensions = append(mdq.MetricStat.Metric.Dimensions,
&cloudwatch.Dimension{
cloudwatchtypes.Dimension{
Name: aws.String(key),
Value: aws.String(values[0]),
})
@ -121,14 +120,14 @@ func buildSearchExpression(query *models.CloudWatchQuery, stat string) string {
}
schema = fmt.Sprintf("{%s}", schema)
schemaSearchTermAndAccount := strings.TrimSpace(strings.Join([]string{schema, searchTerm, account}, " "))
return fmt.Sprintf("REMOVE_EMPTY(SEARCH('%s', '%s', %s))", schemaSearchTermAndAccount, stat, strconv.Itoa(query.Period))
return fmt.Sprintf("REMOVE_EMPTY(SEARCH('%s', '%s', %d))", schemaSearchTermAndAccount, stat, query.Period)
}
sort.Strings(dimensionNamesWithoutKnownValues)
searchTerm = appendSearch(searchTerm, join(dimensionNamesWithoutKnownValues, " ", `"`, `"`))
namespace := fmt.Sprintf("Namespace=%q", query.Namespace)
namespaceSearchTermAndAccount := strings.TrimSpace(strings.Join([]string{namespace, searchTerm, account}, " "))
return fmt.Sprintf(`REMOVE_EMPTY(SEARCH('%s', '%s', %s))`, namespaceSearchTermAndAccount, stat, strconv.Itoa(query.Period))
return fmt.Sprintf(`REMOVE_EMPTY(SEARCH('%s', '%s', %d))`, namespaceSearchTermAndAccount, stat, query.Period)
}
func buildSearchExpressionLabel(query *models.CloudWatchQuery) string {

View File

@ -4,8 +4,8 @@ import (
"context"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/stretchr/testify/assert"
@ -13,13 +13,13 @@ import (
)
func TestMetricDataQueryBuilder(t *testing.T) {
executor := newExecutor(nil, log.NewNullLogger())
ds := newTestDatasource()
t.Run("buildMetricDataQuery", func(t *testing.T) {
t.Run("should use metric stat", func(t *testing.T) {
query := getBaseQuery()
query.MetricEditorMode = models.MetricEditorModeBuilder
query.MetricQueryType = models.MetricQueryTypeSearch
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
require.NoError(t, err)
require.Empty(t, mdq.Expression)
assert.Equal(t, query.MetricName, *mdq.MetricStat.Metric.MetricName)
@ -31,7 +31,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
query.MetricEditorMode = models.MetricEditorModeBuilder
query.MetricQueryType = models.MetricQueryTypeSearch
query.AccountId = aws.String("some account id")
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
require.NoError(t, err)
assert.Equal(t, "some account id", *mdq.AccountId)
})
@ -40,7 +40,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
query := getBaseQuery()
query.MetricEditorMode = models.MetricEditorModeBuilder
query.MetricQueryType = models.MetricQueryTypeSearch
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
require.NoError(t, err)
assert.Nil(t, mdq.AccountId)
})
@ -50,7 +50,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
query.MetricEditorMode = models.MetricEditorModeBuilder
query.MetricQueryType = models.MetricQueryTypeSearch
query.MatchExact = false
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
require.NoError(t, err)
require.Nil(t, mdq.MetricStat)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "LoadBalancer"="lb1"', '', 300))`, *mdq.Expression)
@ -61,7 +61,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
query.MetricEditorMode = models.MetricEditorModeRaw
query.MetricQueryType = models.MetricQueryTypeQuery
query.SqlExpression = `SELECT SUM(CPUUTilization) FROM "AWS/EC2"`
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
require.NoError(t, err)
require.Nil(t, mdq.MetricStat)
assert.Equal(t, query.SqlExpression, *mdq.Expression)
@ -72,32 +72,30 @@ func TestMetricDataQueryBuilder(t *testing.T) {
query.MetricEditorMode = models.MetricEditorModeRaw
query.MetricQueryType = models.MetricQueryTypeSearch
query.Expression = `SUM(x+y)`
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
require.NoError(t, err)
require.Nil(t, mdq.MetricStat)
assert.Equal(t, query.Expression, *mdq.Expression)
})
t.Run("should set period in user defined expression", func(t *testing.T) {
executor := newExecutor(nil, log.NewNullLogger())
query := getBaseQuery()
query.MetricEditorMode = models.MetricEditorModeRaw
query.MetricQueryType = models.MetricQueryTypeSearch
query.MatchExact = false
query.Expression = `SUM([a,b])`
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
require.NoError(t, err)
require.Nil(t, mdq.MetricStat)
assert.Equal(t, int64(300), *mdq.Period)
assert.Equal(t, int32(300), *mdq.Period)
assert.Equal(t, `SUM([a,b])`, *mdq.Expression)
})
t.Run("should set label", func(t *testing.T) {
executor := newExecutor(nil, log.NewNullLogger())
query := getBaseQuery()
query.Label = "some label"
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
assert.NoError(t, err)
require.NotNil(t, mdq.Label)
@ -105,18 +103,16 @@ func TestMetricDataQueryBuilder(t *testing.T) {
})
t.Run("should not set label for empty string query label", func(t *testing.T) {
executor := newExecutor(nil, log.NewNullLogger())
query := getBaseQuery()
query.Label = ""
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
assert.NoError(t, err)
assert.Nil(t, mdq.Label)
})
t.Run(`should not specify accountId when it is "all"`, func(t *testing.T) {
executor := newExecutor(nil, log.NewNullLogger())
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
@ -126,7 +122,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
AccountId: aws.String("all"),
}
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
assert.NoError(t, err)
require.Nil(t, mdq.MetricStat)
@ -134,7 +130,6 @@ func TestMetricDataQueryBuilder(t *testing.T) {
})
t.Run("should set accountId when it is specified", func(t *testing.T) {
executor := newExecutor(nil, log.NewNullLogger())
query := &models.CloudWatchQuery{
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
@ -144,7 +139,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
AccountId: aws.String("12345"),
}
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
assert.NoError(t, err)
require.Nil(t, mdq.MetricStat)
@ -170,7 +165,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
MetricEditorMode: models.MetricEditorModeBuilder,
}
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
require.NoError(t, err)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/EC2","LoadBalancer"} MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, *mdq.Expression)
assert.Equal(t, "${LABEL}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
@ -192,7 +187,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
MetricEditorMode: models.MetricEditorModeBuilder,
}
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
require.NoError(t, err)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/EC2","InstanceId","LoadBalancer"} MetricName="CPUUtilization" "InstanceId"=("i-123" OR "i-456" OR "i-789") "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, *mdq.Expression)
assert.Equal(t, "${LABEL}|&|${PROP('Dim.InstanceId')}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
@ -213,7 +208,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
MetricEditorMode: models.MetricEditorModeBuilder,
}
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
require.NoError(t, err)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/EC2","LoadBalancer"} MetricName="CPUUtilization"', 'Average', 300))`, *mdq.Expression)
assert.Equal(t, "${LABEL}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
@ -235,7 +230,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
MetricEditorMode: models.MetricEditorModeBuilder,
}
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
require.NoError(t, err)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/EC2","InstanceId","LoadBalancer"} MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, *mdq.Expression)
assert.Equal(t, "${LABEL}|&|${PROP('Dim.InstanceId')}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
@ -258,7 +253,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
MetricEditorMode: models.MetricEditorModeBuilder,
}
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
require.NoError(t, err)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/EC2","InstanceId","LoadBalancer"} MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3") :aws.AccountId="some account id"', 'Average', 300))`, *mdq.Expression)
assert.Equal(t, "${LABEL}|&|${PROP('Dim.InstanceId')}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
@ -279,7 +274,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
MetricEditorMode: models.MetricEditorModeBuilder,
}
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
require.NoError(t, err)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/Kafka","Cluster Name"} MetricName="CpuUser" "Cluster Name"=("dev-cluster" OR "prod-cluster")', 'Average', 300))`, *mdq.Expression)
assert.Equal(t, "${LABEL}|&|${PROP('Dim.Cluster Name')}", *mdq.Label)
@ -301,7 +296,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
MetricEditorMode: models.MetricEditorModeBuilder,
}
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
require.NoError(t, err)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"Test-API Cache by Minute","InstanceId","LoadBalancer"} MetricName="CpuUser" "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, *mdq.Expression)
assert.Equal(t, "${LABEL}|&|${PROP('Dim.InstanceId')}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
@ -324,7 +319,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
MetricEditorMode: models.MetricEditorModeBuilder,
}
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
require.NoError(t, err)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"CPUUtilization","InstanceId","LoadBalancer"} MetricName="CpuUser" "LoadBalancer"="lb1"', 'Average', 300))`, *mdq.Expression)
assert.Equal(t, "LB: ${PROP('Dim.LoadBalancer')|&|${PROP('Dim.InstanceId')}", *mdq.Label)
@ -349,7 +344,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
MetricEditorMode: models.MetricEditorModeBuilder,
}
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
require.NoError(t, err)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, *mdq.Expression)
assert.Equal(t, "${LABEL}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
@ -371,7 +366,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
MetricEditorMode: models.MetricEditorModeBuilder,
}
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
require.NoError(t, err)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "InstanceId"=("i-123" OR "i-456" OR "i-789") "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, *mdq.Expression)
assert.Equal(t, "${LABEL}|&|${PROP('Dim.InstanceId')}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
@ -392,7 +387,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
MetricEditorMode: models.MetricEditorModeBuilder,
}
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
require.NoError(t, err)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "LoadBalancer"', 'Average', 300))`, *mdq.Expression)
assert.Equal(t, "${LABEL}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
@ -414,7 +409,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
MetricEditorMode: models.MetricEditorModeBuilder,
}
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
require.NoError(t, err)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3") "InstanceId"', 'Average', 300))`, *mdq.Expression)
assert.Equal(t, "${LABEL}|&|${PROP('Dim.InstanceId')}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
@ -437,7 +432,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
MetricEditorMode: models.MetricEditorModeBuilder,
}
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
require.NoError(t, err)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3") "InstanceId" :aws.AccountId="some account id"', 'Average', 300))`, *mdq.Expression)
assert.Equal(t, "${LABEL}|&|${PROP('Dim.InstanceId')}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
@ -460,7 +455,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
MetricEditorMode: models.MetricEditorModeBuilder,
}
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
require.NoError(t, err)
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "LoadBalancer"="lb1" "InstanceId"', 'Average', 300))`, *mdq.Expression)
assert.Equal(t, "LB: ${PROP('Dim.LoadBalancer')|&|${PROP('Dim.InstanceId')}", *mdq.Label)

View File

@ -12,11 +12,13 @@ import (
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
resourcegroupstaggingapitypes "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
)
type suggestData struct {
@ -39,12 +41,12 @@ func parseMultiSelectValue(input string) []string {
return []string{trimmedInput}
}
func (e *cloudWatchExecutor) handleGetEbsVolumeIds(ctx context.Context, pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) {
func (ds *DataSource) handleGetEbsVolumeIds(ctx context.Context, parameters url.Values) ([]suggestData, error) {
region := parameters.Get("region")
instanceId := parameters.Get("instanceId")
instanceIds := aws.StringSlice(parseMultiSelectValue(instanceId))
instances, err := e.ec2DescribeInstances(ctx, pluginCtx, region, nil, instanceIds)
instanceIds := parseMultiSelectValue(instanceId)
instances, err := ds.ec2DescribeInstances(ctx, region, nil, instanceIds)
if err != nil {
return nil, err
}
@ -61,7 +63,7 @@ func (e *cloudWatchExecutor) handleGetEbsVolumeIds(ctx context.Context, pluginCt
return result, nil
}
func (e *cloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context, pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) {
func (ds *DataSource) handleGetEc2InstanceAttribute(ctx context.Context, parameters url.Values) ([]suggestData, error) {
region := parameters.Get("region")
attributeName := parameters.Get("attributeName")
filterJson := parameters.Get("filters")
@ -72,23 +74,23 @@ func (e *cloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context,
return nil, fmt.Errorf("error unmarshaling filter: %v", err)
}
var filters []*ec2.Filter
var filters []ec2types.Filter
for k, v := range filterMap {
if vv, ok := v.([]any); ok {
var values []*string
var values []string
for _, vvv := range vv {
if vvvv, ok := vvv.(string); ok {
values = append(values, &vvvv)
values = append(values, vvvv)
}
}
filters = append(filters, &ec2.Filter{
filters = append(filters, ec2types.Filter{
Name: aws.String(k),
Values: values,
})
}
}
instances, err := e.ec2DescribeInstances(ctx, pluginCtx, region, filters, nil)
instances, err := ds.ec2DescribeInstances(ctx, region, filters, nil)
if err != nil {
return nil, err
}
@ -120,7 +122,7 @@ func (e *cloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context,
return result, nil
}
func getInstanceAttributeValue(attributeName string, instance *ec2.Instance) (value string, found bool, err error) {
func getInstanceAttributeValue(attributeName string, instance ec2types.Instance) (value string, found bool, err error) {
tags := make(map[string]string)
for _, tag := range instance.Tags {
tags[*tag.Key] = *tag.Value
@ -152,7 +154,12 @@ func getInstanceAttributeValue(attributeName string, instance *ec2.Instance) (va
if v.Kind() == reflect.Ptr && v.IsNil() {
return "", false, nil
}
if attr, ok := v.Interface().(*string); ok {
if v.Kind() == reflect.String {
if v.String() == "" {
return "", false, nil
}
data = v.String()
} else if attr, ok := v.Interface().(*string); ok {
data = *attr
} else if attr, ok := v.Interface().(*time.Time); ok {
data = attr.String()
@ -168,7 +175,7 @@ func getInstanceAttributeValue(attributeName string, instance *ec2.Instance) (va
return data, true, nil
}
func (e *cloudWatchExecutor) handleGetResourceArns(ctx context.Context, pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) {
func (ds *DataSource) handleGetResourceArns(ctx context.Context, parameters url.Values) ([]suggestData, error) {
region := parameters.Get("region")
resourceType := parameters.Get("resourceType")
tagsJson := parameters.Get("tags")
@ -179,26 +186,25 @@ func (e *cloudWatchExecutor) handleGetResourceArns(ctx context.Context, pluginCt
return nil, fmt.Errorf("error unmarshaling filter: %v", err)
}
var filters []*resourcegroupstaggingapi.TagFilter
var filters []resourcegroupstaggingapitypes.TagFilter
for k, v := range tagsMap {
if vv, ok := v.([]any); ok {
var values []*string
var values []string
for _, vvv := range vv {
if vvvv, ok := vvv.(string); ok {
values = append(values, &vvvv)
values = append(values, vvvv)
}
}
filters = append(filters, &resourcegroupstaggingapi.TagFilter{
filters = append(filters, resourcegroupstaggingapitypes.TagFilter{
Key: aws.String(k),
Values: values,
})
}
}
var resourceTypes []*string
resourceTypes = append(resourceTypes, &resourceType)
resourceTypes := []string{resourceType}
resources, err := e.resourceGroupsGetResources(ctx, pluginCtx, region, filters, resourceTypes)
resources, err := ds.resourceGroupsGetResources(ctx, region, filters, resourceTypes)
if err != nil {
return nil, err
}
@ -212,75 +218,77 @@ func (e *cloudWatchExecutor) handleGetResourceArns(ctx context.Context, pluginCt
return result, nil
}
func (e *cloudWatchExecutor) ec2DescribeInstances(ctx context.Context, pluginCtx backend.PluginContext, region string, filters []*ec2.Filter, instanceIds []*string) (*ec2.DescribeInstancesOutput, error) {
func (ds *DataSource) ec2DescribeInstances(ctx context.Context, region string, filters []ec2types.Filter, instanceIds []string) (*ec2.DescribeInstancesOutput, error) {
params := &ec2.DescribeInstancesInput{
Filters: filters,
InstanceIds: instanceIds,
}
client, err := e.getEC2Client(ctx, pluginCtx, region)
client, err := ds.getEC2Client(ctx, region)
if err != nil {
return nil, err
}
var resp ec2.DescribeInstancesOutput
if err := client.DescribeInstancesPagesWithContext(ctx, params, func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
resp := &ec2.DescribeInstancesOutput{}
pager := ec2.NewDescribeInstancesPaginator(client, params)
for pager.HasMorePages() {
page, err := pager.NextPage(ctx)
if err != nil {
return resp, fmt.Errorf("describe instances pager failed: %w", err)
}
resp.Reservations = append(resp.Reservations, page.Reservations...)
return !lastPage
}); err != nil {
return nil, fmt.Errorf("failed to call ec2:DescribeInstances, %w", err)
}
return &resp, nil
return resp, nil
}
func (e *cloudWatchExecutor) resourceGroupsGetResources(ctx context.Context, pluginCtx backend.PluginContext, region string, filters []*resourcegroupstaggingapi.TagFilter,
resourceTypes []*string) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
func (ds *DataSource) resourceGroupsGetResources(ctx context.Context, region string, filters []resourcegroupstaggingapitypes.TagFilter,
resourceTypes []string) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
params := &resourcegroupstaggingapi.GetResourcesInput{
ResourceTypeFilters: resourceTypes,
TagFilters: filters,
}
client, err := e.getRGTAClient(ctx, pluginCtx, region)
client, err := ds.getRGTAClient(ctx, region)
if err != nil {
return nil, err
}
var resp resourcegroupstaggingapi.GetResourcesOutput
if err := client.GetResourcesPagesWithContext(ctx, params,
func(page *resourcegroupstaggingapi.GetResourcesOutput, lastPage bool) bool {
resp.ResourceTagMappingList = append(resp.ResourceTagMappingList, page.ResourceTagMappingList...)
return !lastPage
}); err != nil {
return nil, fmt.Errorf("failed to call tag:GetResources, %w", err)
paginator := resourcegroupstaggingapi.NewGetResourcesPaginator(client, params)
for paginator.HasMorePages() {
page, err := paginator.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("get resource groups paginator failed: %w", err)
}
resp.ResourceTagMappingList = append(resp.ResourceTagMappingList, page.ResourceTagMappingList...)
}
return &resp, nil
}
// legacy route, will be removed once GovCloud supports Cross Account Observability
func (e *cloudWatchExecutor) handleGetLogGroups(ctx context.Context, pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) {
func (ds *DataSource) handleGetLogGroups(ctx context.Context, parameters url.Values) ([]suggestData, error) {
region := parameters.Get("region")
limit := parameters.Get("limit")
logGroupNamePrefix := parameters.Get("logGroupNamePrefix")
logsClient, err := e.getCWLogsClient(ctx, pluginCtx, region)
logsClient, err := ds.getCWLogsClient(ctx, region)
if err != nil {
return nil, err
}
logGroupLimit := defaultLogGroupLimit
intLimit, err := strconv.ParseInt(limit, 10, 64)
intLimit, err := strconv.ParseInt(limit, 10, 32)
if err == nil && intLimit > 0 {
logGroupLimit = intLimit
logGroupLimit = int32(intLimit)
}
input := &cloudwatchlogs.DescribeLogGroupsInput{Limit: aws.Int64(logGroupLimit)}
input := &cloudwatchlogs.DescribeLogGroupsInput{Limit: aws.Int32(logGroupLimit)}
if len(logGroupNamePrefix) > 0 {
input.LogGroupNamePrefix = aws.String(logGroupNamePrefix)
}
var response *cloudwatchlogs.DescribeLogGroupsOutput
response, err = logsClient.DescribeLogGroupsWithContext(ctx, input)
response, err = logsClient.DescribeLogGroups(ctx, input)
if err != nil || response == nil {
return nil, err
}

View File

@ -6,41 +6,37 @@ import (
"net/url"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/aws/aws-sdk-go-v2/aws"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
resourcegroupstaggingapitypes "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestQuery_InstanceAttributes(t *testing.T) {
origNewEC2Client := NewEC2Client
origNewEC2API := NewEC2API
t.Cleanup(func() {
NewEC2Client = origNewEC2Client
NewEC2API = origNewEC2API
})
var cli oldEC2Client
NewEC2Client = func(client.ConfigProvider) models.EC2APIProvider {
NewEC2API = func(aws.Config) models.EC2APIProvider {
return cli
}
t.Run("Get instance ID", func(t *testing.T) {
const instanceID = "i-12345678"
cli = oldEC2Client{
reservations: []*ec2.Reservation{
reservations: []ec2types.Reservation{
{
Instances: []*ec2.Instance{
Instances: []ec2types.Instance{
{
InstanceId: aws.String(instanceID),
Tags: []*ec2.Tag{
Tags: []ec2types.Tag{
{
Key: aws.String("Environment"),
Value: aws.String("production"),
@ -52,22 +48,16 @@ func TestQuery_InstanceAttributes(t *testing.T) {
},
}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
filterMap := map[string][]string{
"tag:Environment": {"production"},
}
filterJson, err := json.Marshal(filterMap)
require.NoError(t, err)
executor := newExecutor(im, log.NewNullLogger())
resp, err := executor.handleGetEc2InstanceAttribute(
ds := newTestDatasource()
resp, err := ds.handleGetEc2InstanceAttribute(
context.Background(),
backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
}, url.Values{
url.Values{
"region": []string{"us-east-1"},
"attributeName": []string{"InstanceId"},
"filters": []string{string(filterJson)},
@ -82,17 +72,17 @@ func TestQuery_InstanceAttributes(t *testing.T) {
})
t.Run("Get different types", func(t *testing.T) {
var expectedInt int64 = 3
var expectedInt int32 = 3
var expectedBool = true
var expectedArn = "arn"
cli = oldEC2Client{
reservations: []*ec2.Reservation{
reservations: []ec2types.Reservation{
{
Instances: []*ec2.Instance{
Instances: []ec2types.Instance{
{
AmiLaunchIndex: &expectedInt,
EbsOptimized: &expectedBool,
IamInstanceProfile: &ec2.IamInstanceProfile{
IamInstanceProfile: &ec2types.IamInstanceProfile{
Arn: &expectedArn,
},
},
@ -101,11 +91,7 @@ func TestQuery_InstanceAttributes(t *testing.T) {
},
}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
executor := newExecutor(im, log.NewNullLogger())
ds := newTestDatasource()
testcases := []struct {
name string
@ -145,11 +131,9 @@ func TestQuery_InstanceAttributes(t *testing.T) {
filterJson, err := json.Marshal(filterMap)
require.NoError(t, err)
resp, err := executor.handleGetEc2InstanceAttribute(
resp, err := ds.handleGetEc2InstanceAttribute(
context.Background(),
backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
}, url.Values{
url.Values{
"region": []string{"us-east-1"},
"attributeName": []string{tc.attributeName},
"filters": []string{string(filterJson)},
@ -163,52 +147,52 @@ func TestQuery_InstanceAttributes(t *testing.T) {
}
func TestQuery_EBSVolumeIDs(t *testing.T) {
origNewEC2Client := NewEC2Client
origNewEC2API := NewEC2API
t.Cleanup(func() {
NewEC2Client = origNewEC2Client
NewEC2API = origNewEC2API
})
var cli oldEC2Client
NewEC2Client = func(client.ConfigProvider) models.EC2APIProvider {
NewEC2API = func(aws.Config) models.EC2APIProvider {
return cli
}
t.Run("", func(t *testing.T) {
cli = oldEC2Client{
reservations: []*ec2.Reservation{
reservations: []ec2types.Reservation{
{
Instances: []*ec2.Instance{
Instances: []ec2types.Instance{
{
InstanceId: aws.String("i-1"),
BlockDeviceMappings: []*ec2.InstanceBlockDeviceMapping{
{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-1-1")}},
{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-1-2")}},
BlockDeviceMappings: []ec2types.InstanceBlockDeviceMapping{
{Ebs: &ec2types.EbsInstanceBlockDevice{VolumeId: aws.String("vol-1-1")}},
{Ebs: &ec2types.EbsInstanceBlockDevice{VolumeId: aws.String("vol-1-2")}},
},
},
{
InstanceId: aws.String("i-2"),
BlockDeviceMappings: []*ec2.InstanceBlockDeviceMapping{
{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-2-1")}},
{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-2-2")}},
BlockDeviceMappings: []ec2types.InstanceBlockDeviceMapping{
{Ebs: &ec2types.EbsInstanceBlockDevice{VolumeId: aws.String("vol-2-1")}},
{Ebs: &ec2types.EbsInstanceBlockDevice{VolumeId: aws.String("vol-2-2")}},
},
},
},
},
{
Instances: []*ec2.Instance{
Instances: []ec2types.Instance{
{
InstanceId: aws.String("i-3"),
BlockDeviceMappings: []*ec2.InstanceBlockDeviceMapping{
{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-3-1")}},
{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-3-2")}},
BlockDeviceMappings: []ec2types.InstanceBlockDeviceMapping{
{Ebs: &ec2types.EbsInstanceBlockDevice{VolumeId: aws.String("vol-3-1")}},
{Ebs: &ec2types.EbsInstanceBlockDevice{VolumeId: aws.String("vol-3-2")}},
},
},
{
InstanceId: aws.String("i-4"),
BlockDeviceMappings: []*ec2.InstanceBlockDeviceMapping{
{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-4-1")}},
{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-4-2")}},
BlockDeviceMappings: []ec2types.InstanceBlockDeviceMapping{
{Ebs: &ec2types.EbsInstanceBlockDevice{VolumeId: aws.String("vol-4-1")}},
{Ebs: &ec2types.EbsInstanceBlockDevice{VolumeId: aws.String("vol-4-2")}},
},
},
},
@ -216,16 +200,10 @@ func TestQuery_EBSVolumeIDs(t *testing.T) {
},
}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
executor := newExecutor(im, log.NewNullLogger())
resp, err := executor.handleGetEbsVolumeIds(
ds := newTestDatasource()
resp, err := ds.handleGetEbsVolumeIds(
context.Background(),
backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
}, url.Values{
url.Values{
"region": []string{"us-east-1"},
"instanceId": []string{"{i-1, i-2, i-3}"},
},
@ -242,23 +220,23 @@ func TestQuery_EBSVolumeIDs(t *testing.T) {
}
func TestQuery_ResourceARNs(t *testing.T) {
origNewRGTAClient := newRGTAClient
origNewRGTAClient := NewRGTAClient
t.Cleanup(func() {
newRGTAClient = origNewRGTAClient
NewRGTAClient = origNewRGTAClient
})
var cli fakeRGTAClient
newRGTAClient = func(client.ConfigProvider) resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI {
NewRGTAClient = func(aws.Config) resourcegroupstaggingapi.GetResourcesAPIClient {
return cli
}
t.Run("", func(t *testing.T) {
cli = fakeRGTAClient{
tagMapping: []*resourcegroupstaggingapi.ResourceTagMapping{
tagMapping: []resourcegroupstaggingapitypes.ResourceTagMapping{
{
ResourceARN: aws.String("arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567"),
Tags: []*resourcegroupstaggingapi.Tag{
Tags: []resourcegroupstaggingapitypes.Tag{
{
Key: aws.String("Environment"),
Value: aws.String("production"),
@ -267,7 +245,7 @@ func TestQuery_ResourceARNs(t *testing.T) {
},
{
ResourceARN: aws.String("arn:aws:ec2:us-east-1:123456789012:instance/i-76543210987654321"),
Tags: []*resourcegroupstaggingapi.Tag{
Tags: []resourcegroupstaggingapitypes.Tag{
{
Key: aws.String("Environment"),
Value: aws.String("production"),
@ -277,22 +255,16 @@ func TestQuery_ResourceARNs(t *testing.T) {
},
}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
tagMap := map[string][]string{
"Environment": {"production"},
}
tagJson, err := json.Marshal(tagMap)
require.NoError(t, err)
executor := newExecutor(im, log.NewNullLogger())
resp, err := executor.handleGetResourceArns(
ds := newTestDatasource()
resp, err := ds.handleGetResourceArns(
context.Background(),
backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
}, url.Values{
url.Values{
"region": []string{"us-east-1"},
"resourceType": []string{"ec2:instance"},
"tags": []string{string(tagJson)},

View File

@ -1,13 +1,11 @@
package routes
package cloudwatch
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
@ -17,15 +15,21 @@ import (
)
func Test_Metrics_Route(t *testing.T) {
origNewListMetricsServices := services.NewListMetricsService
t.Cleanup(func() {
services.NewListMetricsService = origNewListMetricsServices
})
var mockListMetricsService mocks.ListMetricsServiceMock
services.NewListMetricsService = func(provider models.MetricsClientProvider) models.ListMetricsProvider {
return &mockListMetricsService
}
t.Run("calls GetMetricsByNamespace when a CustomNamespaceRequestType is passed", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService = mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetMetricsByNamespace", mock.Anything).Return([]resources.ResourceResponse[resources.Metric]{}, nil)
newListMetricsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/metrics?region=us-east-2&namespace=customNamespace", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(MetricsHandler, logger, nil))
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.MetricsHandler))
handler.ServeHTTP(rr, req)
mockListMetricsService.AssertNumberOfCalls(t, "GetMetricsByNamespace", 1)
})
@ -42,7 +46,8 @@ func Test_Metrics_Route(t *testing.T) {
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/metrics?region=us-east-2", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(MetricsHandler, logger, nil))
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.MetricsHandler))
handler.ServeHTTP(rr, req)
assert.True(t, haveBeenCalled)
})
@ -61,21 +66,20 @@ func Test_Metrics_Route(t *testing.T) {
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/metrics?region=us-east-2&namespace=AWS/DMS", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(MetricsHandler, logger, nil))
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.MetricsHandler))
handler.ServeHTTP(rr, req)
assert.True(t, haveBeenCalled)
assert.Equal(t, "AWS/DMS", usedNamespace)
})
t.Run("returns 500 if GetMetricsByNamespace returns an error", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService = mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetMetricsByNamespace", mock.Anything).Return([]resources.ResourceResponse[resources.Metric]{}, fmt.Errorf("some error"))
newListMetricsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/metrics?region=us-east-2&namespace=customNamespace", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(MetricsHandler, logger, nil))
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.MetricsHandler))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
assert.Equal(t, `{"Message":"error in MetricsHandler: some error","Error":"some error","StatusCode":500}`, rr.Body.String())

View File

@ -0,0 +1,39 @@
package cloudwatch
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
)
func Test_Middleware(t *testing.T) {
t.Run("rejects POST method", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/dimension-keys?region=us-east-1", nil)
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(func(_ context.Context, parameters url.Values) ([]byte, *models.HttpError) {
return []byte{}, nil
}))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusMethodNotAllowed, rr.Code)
})
t.Run("should propagate handler error to response", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/some-path", nil)
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(func(_ context.Context, parameters url.Values) ([]byte, *models.HttpError) {
return []byte{}, models.NewHttpError("error", http.StatusBadRequest, fmt.Errorf("error from handler"))
}))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Equal(t, `{"Message":"error: error from handler","Error":"error from handler","StatusCode":400}`, rr.Body.String())
})
}

View File

@ -11,7 +11,7 @@ type AccountsServiceMock struct {
mock.Mock
}
func (a *AccountsServiceMock) GetAccountsForCurrentUserOrRole(ctx context.Context) ([]resources.ResourceResponse[resources.Account], error) {
func (a *AccountsServiceMock) GetAccountsForCurrentUserOrRole(_ context.Context) ([]resources.ResourceResponse[resources.Account], error) {
args := a.Called()
return args.Get(0).([]resources.ResourceResponse[resources.Account]), args.Error(1)

View File

@ -1,68 +1,65 @@
package mocks
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/stretchr/testify/mock"
)
type FakeMetricsAPI struct {
Metrics []*cloudwatch.Metric
OwningAccounts []*string
models.CWClient
Metrics []cloudwatchtypes.Metric
OwningAccounts []string
MetricsPerPage int
cursor int
}
func (c *FakeMetricsAPI) ListMetricsPagesWithContext(ctx aws.Context, input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool, opts ...request.Option) error {
func (c *FakeMetricsAPI) ListMetrics(_ context.Context, _ *cloudwatch.ListMetricsInput, _ ...func(*cloudwatch.Options)) (*cloudwatch.ListMetricsOutput, error) {
if c.MetricsPerPage == 0 {
c.MetricsPerPage = 1000
}
chunks := chunkSlice(c.Metrics, c.MetricsPerPage)
for i, metrics := range chunks {
response := fn(&cloudwatch.ListMetricsOutput{
Metrics: metrics,
OwningAccounts: c.OwningAccounts,
}, i+1 == len(chunks))
if !response {
break
var metrics []cloudwatchtypes.Metric
nextToken := aws.String("yes")
if c.cursor < len(c.Metrics) {
end := c.cursor + c.MetricsPerPage
if end > len(c.Metrics) {
end = len(c.Metrics)
nextToken = nil
}
metrics = c.Metrics[c.cursor:end]
}
return nil
}
c.cursor += c.MetricsPerPage
func chunkSlice(slice []*cloudwatch.Metric, chunkSize int) [][]*cloudwatch.Metric {
var chunks [][]*cloudwatch.Metric
for len(slice) != 0 {
if len(slice) < chunkSize {
chunkSize = len(slice)
}
chunks = append(chunks, slice[0:chunkSize])
slice = slice[chunkSize:]
}
return chunks
return &cloudwatch.ListMetricsOutput{
Metrics: metrics,
OwningAccounts: c.OwningAccounts,
NextToken: nextToken,
}, nil
}
type MetricsAPI struct {
cloudwatchiface.CloudWatchAPI
mock.Mock
models.CWClient
Metrics []*cloudwatch.Metric
Metrics []cloudwatchtypes.Metric
}
func (m *MetricsAPI) GetMetricDataWithContext(ctx aws.Context, input *cloudwatch.GetMetricDataInput, opts ...request.Option) (*cloudwatch.GetMetricDataOutput, error) {
args := m.Called(ctx, input, opts)
func (m *MetricsAPI) GetMetricData(ctx context.Context, input *cloudwatch.GetMetricDataInput, optFns ...func(*cloudwatch.Options)) (*cloudwatch.GetMetricDataOutput, error) {
args := m.Called(ctx, input, optFns)
return args.Get(0).(*cloudwatch.GetMetricDataOutput), args.Error(1)
}
func (m *MetricsAPI) ListMetricsPagesWithContext(ctx aws.Context, input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool, opts ...request.Option) error {
fn(&cloudwatch.ListMetricsOutput{
func (m *MetricsAPI) ListMetrics(_ context.Context, _ *cloudwatch.ListMetricsInput, _ ...func(*cloudwatch.Options)) (*cloudwatch.ListMetricsOutput, error) {
return &cloudwatch.ListMetricsOutput{
Metrics: m.Metrics,
}, true)
return m.Called().Error(0)
}, m.Called().Error(0)
}

View File

@ -11,19 +11,19 @@ type ListMetricsServiceMock struct {
mock.Mock
}
func (a *ListMetricsServiceMock) GetDimensionKeysByDimensionFilter(ctx context.Context, r resources.DimensionKeysRequest) ([]resources.ResourceResponse[string], error) {
func (a *ListMetricsServiceMock) GetDimensionKeysByDimensionFilter(_ context.Context, r resources.DimensionKeysRequest) ([]resources.ResourceResponse[string], error) {
args := a.Called(r)
return args.Get(0).([]resources.ResourceResponse[string]), args.Error(1)
}
func (a *ListMetricsServiceMock) GetDimensionValuesByDimensionFilter(ctx context.Context, r resources.DimensionValuesRequest) ([]resources.ResourceResponse[string], error) {
func (a *ListMetricsServiceMock) GetDimensionValuesByDimensionFilter(_ context.Context, r resources.DimensionValuesRequest) ([]resources.ResourceResponse[string], error) {
args := a.Called(r)
return args.Get(0).([]resources.ResourceResponse[string]), args.Error(1)
}
func (a *ListMetricsServiceMock) GetMetricsByNamespace(ctx context.Context, r resources.MetricsRequest) ([]resources.ResourceResponse[resources.Metric], error) {
func (a *ListMetricsServiceMock) GetMetricsByNamespace(_ context.Context, r resources.MetricsRequest) ([]resources.ResourceResponse[resources.Metric], error) {
args := a.Called(r)
return args.Get(0).([]resources.ResourceResponse[resources.Metric]), args.Error(1)

View File

@ -3,10 +3,7 @@ package mocks
import (
"context"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
"github.com/stretchr/testify/mock"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
@ -16,13 +13,13 @@ type LogsAPI struct {
mock.Mock
}
func (l *LogsAPI) DescribeLogGroupsWithContext(ctx context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, option ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
func (l *LogsAPI) DescribeLogGroups(_ context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
args := l.Called(input)
return args.Get(0).(*cloudwatchlogs.DescribeLogGroupsOutput), args.Error(1)
}
func (l *LogsAPI) GetLogGroupFieldsWithContext(ctx context.Context, input *cloudwatchlogs.GetLogGroupFieldsInput, option ...request.Option) (*cloudwatchlogs.GetLogGroupFieldsOutput, error) {
func (l *LogsAPI) GetLogGroupFields(_ context.Context, input *cloudwatchlogs.GetLogGroupFieldsInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetLogGroupFieldsOutput, error) {
args := l.Called(input)
return args.Get(0).(*cloudwatchlogs.GetLogGroupFieldsOutput), args.Error(1)
@ -32,26 +29,40 @@ type LogsService struct {
mock.Mock
}
func (l *LogsService) GetLogGroupsWithContext(ctx context.Context, request resources.LogGroupsRequest) ([]resources.ResourceResponse[resources.LogGroup], error) {
func (l *LogsService) GetLogGroups(_ context.Context, request resources.LogGroupsRequest) ([]resources.ResourceResponse[resources.LogGroup], error) {
args := l.Called(request)
return args.Get(0).([]resources.ResourceResponse[resources.LogGroup]), args.Error(1)
}
func (l *LogsService) GetLogGroupFieldsWithContext(ctx context.Context, request resources.LogGroupFieldsRequest, option ...request.Option) ([]resources.ResourceResponse[resources.LogGroupField], error) {
func (l *LogsService) GetLogGroupFields(_ context.Context, request resources.LogGroupFieldsRequest) ([]resources.ResourceResponse[resources.LogGroupField], error) {
args := l.Called(request)
return args.Get(0).([]resources.ResourceResponse[resources.LogGroupField]), args.Error(1)
}
type MockLogEvents struct {
cloudwatchlogsiface.CloudWatchLogsAPI
mock.Mock
}
func (m *MockLogEvents) GetLogEventsWithContext(ctx aws.Context, input *cloudwatchlogs.GetLogEventsInput, option ...request.Option) (*cloudwatchlogs.GetLogEventsOutput, error) {
args := m.Called(ctx, input, option)
func (m *MockLogEvents) StartQuery(context.Context, *cloudwatchlogs.StartQueryInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.StartQueryOutput, error) {
return nil, nil
}
func (m *MockLogEvents) StopQuery(context.Context, *cloudwatchlogs.StopQueryInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.StopQueryOutput, error) {
return nil, nil
}
func (m *MockLogEvents) GetQueryResults(context.Context, *cloudwatchlogs.GetQueryResultsInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetQueryResultsOutput, error) {
return nil, nil
}
func (m *MockLogEvents) DescribeLogGroups(context.Context, *cloudwatchlogs.DescribeLogGroupsInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
return nil, nil
}
func (m *MockLogEvents) GetLogEvents(ctx context.Context, input *cloudwatchlogs.GetLogEventsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetLogEventsOutput, error) {
args := m.Called(ctx, input, optFns)
return args.Get(0).(*cloudwatchlogs.GetLogEventsOutput), args.Error(1)
}

View File

@ -3,7 +3,8 @@ package mocks
import (
"context"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/stretchr/testify/mock"
)
@ -12,7 +13,7 @@ type FakeMetricsClient struct {
mock.Mock
}
func (m *FakeMetricsClient) ListMetricsWithPageLimit(ctx context.Context, params *cloudwatch.ListMetricsInput) ([]resources.MetricResponse, error) {
func (m *FakeMetricsClient) ListMetricsWithPageLimit(_ context.Context, params *cloudwatch.ListMetricsInput) ([]resources.MetricResponse, error) {
args := m.Called(params)
return args.Get(0).([]resources.MetricResponse), args.Error(1)
}

View File

@ -3,8 +3,7 @@ package mocks
import (
"context"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/oam"
"github.com/aws/aws-sdk-go-v2/service/oam"
"github.com/stretchr/testify/mock"
)
@ -12,12 +11,12 @@ type FakeOAMClient struct {
mock.Mock
}
func (o *FakeOAMClient) ListSinksWithContext(ctx context.Context, input *oam.ListSinksInput, opts ...request.Option) (*oam.ListSinksOutput, error) {
func (o *FakeOAMClient) ListSinks(_ context.Context, input *oam.ListSinksInput, _ ...func(*oam.Options)) (*oam.ListSinksOutput, error) {
args := o.Called(input)
return args.Get(0).(*oam.ListSinksOutput), args.Error(1)
}
func (o *FakeOAMClient) ListAttachedLinksWithContext(ctx context.Context, input *oam.ListAttachedLinksInput, opts ...request.Option) (*oam.ListAttachedLinksOutput, error) {
func (o *FakeOAMClient) ListAttachedLinks(_ context.Context, input *oam.ListAttachedLinksInput, _ ...func(*oam.Options)) (*oam.ListAttachedLinksOutput, error) {
args := o.Called(input)
return args.Get(0).(*oam.ListAttachedLinksOutput), args.Error(1)
}

View File

@ -3,9 +3,7 @@ package mocks
import (
"context"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/stretchr/testify/mock"
)
@ -14,21 +12,21 @@ type RegionsService struct {
mock.Mock
}
func (r *RegionsService) GetRegions(ctx context.Context) (in []resources.ResourceResponse[resources.Region], e error) {
func (r *RegionsService) GetRegions(_ context.Context) (in []resources.ResourceResponse[resources.Region], e error) {
args := r.Called()
return args.Get(0).(([]resources.ResourceResponse[resources.Region])), args.Error(1)
return args.Get(0).([]resources.ResourceResponse[resources.Region]), args.Error(1)
}
type EC2Mock struct {
mock.Mock
}
func (e *EC2Mock) DescribeRegionsWithContext(ctx aws.Context, in *ec2.DescribeRegionsInput, opts ...request.Option) (*ec2.DescribeRegionsOutput, error) {
func (e *EC2Mock) DescribeRegions(_ context.Context, _ *ec2.DescribeRegionsInput, _ ...func(*ec2.Options)) (*ec2.DescribeRegionsOutput, error) {
args := e.Called()
return args.Get(0).(*ec2.DescribeRegionsOutput), args.Error(1)
}
func (e *EC2Mock) DescribeInstancesPagesWithContext(ctx aws.Context, in *ec2.DescribeInstancesInput, fn func(*ec2.DescribeInstancesOutput, bool) bool, opts ...request.Option) error {
args := e.Called(in, fn)
return args.Error(0)
func (e *EC2Mock) DescribeInstances(_ context.Context, in *ec2.DescribeInstancesInput, _ ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
args := e.Called(in)
return nil, args.Error(0)
}

View File

@ -4,30 +4,29 @@ import (
"context"
"net/url"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/oam"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/oam"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
)
type RequestContextFactoryFunc func(ctx context.Context, pluginCtx backend.PluginContext, region string) (reqCtx RequestContext, err error)
type RequestContextFactoryFunc func(ctx context.Context, region string) (reqCtx RequestContext, err error)
type RouteHandlerFunc func(ctx context.Context, pluginCtx backend.PluginContext, reqContextFactory RequestContextFactoryFunc, parameters url.Values) ([]byte, *HttpError)
type RouteHandlerFunc func(ctx context.Context, parameters url.Values) ([]byte, *HttpError)
type RequestContext struct {
MetricsClientProvider MetricsClientProvider
LogsAPIProvider CloudWatchLogsAPIProvider
OAMAPIProvider OAMAPIProvider
EC2APIProvider EC2APIProvider
Settings CloudWatchSettings
Logger log.Logger
MetricsClientProvider MetricsClientProvider
ListMetricsAPIProvider cloudwatch.ListMetricsAPIClient
LogsAPIProvider CloudWatchLogsAPIProvider
OAMAPIProvider OAMAPIProvider
EC2APIProvider EC2APIProvider
Settings CloudWatchSettings
Logger log.Logger
}
// Services
type ListMetricsProvider interface {
GetDimensionKeysByDimensionFilter(ctx context.Context, r resources.DimensionKeysRequest) ([]resources.ResourceResponse[string], error)
GetDimensionValuesByDimensionFilter(ctx context.Context, r resources.DimensionValuesRequest) ([]resources.ResourceResponse[string], error)
@ -35,8 +34,8 @@ type ListMetricsProvider interface {
}
type LogGroupsProvider interface {
GetLogGroupsWithContext(ctx context.Context, request resources.LogGroupsRequest) ([]resources.ResourceResponse[resources.LogGroup], error)
GetLogGroupFieldsWithContext(ctx context.Context, request resources.LogGroupFieldsRequest, option ...request.Option) ([]resources.ResourceResponse[resources.LogGroupField], error)
GetLogGroups(ctx context.Context, request resources.LogGroupsRequest) ([]resources.ResourceResponse[resources.LogGroup], error)
GetLogGroupFields(ctx context.Context, request resources.LogGroupFieldsRequest) ([]resources.ResourceResponse[resources.LogGroupField], error)
}
type AccountsProvider interface {
@ -47,27 +46,47 @@ type RegionsAPIProvider interface {
GetRegions(ctx context.Context) ([]resources.ResourceResponse[resources.Region], error)
}
// Clients
type MetricsClientProvider interface {
ListMetricsWithPageLimit(ctx context.Context, params *cloudwatch.ListMetricsInput) ([]resources.MetricResponse, error)
}
// APIs - instead of using the API defined in the services within the aws-sdk-go directly, specify a subset of the API with methods that are actually used in a service or a client
type CloudWatchMetricsAPIProvider interface {
ListMetricsPagesWithContext(ctx context.Context, in *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool, opts ...request.Option) error
ListMetrics(ctx context.Context, in *cloudwatch.ListMetricsInput, optFns ...func(*cloudwatch.Options)) error
}
type CloudWatchLogsAPIProvider interface {
DescribeLogGroupsWithContext(ctx context.Context, in *cloudwatchlogs.DescribeLogGroupsInput, opts ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error)
GetLogGroupFieldsWithContext(ctx context.Context, in *cloudwatchlogs.GetLogGroupFieldsInput, option ...request.Option) (*cloudwatchlogs.GetLogGroupFieldsOutput, error)
cloudwatchlogs.DescribeLogGroupsAPIClient
GetLogGroupFields(ctx context.Context, in *cloudwatchlogs.GetLogGroupFieldsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetLogGroupFieldsOutput, error)
}
type OAMAPIProvider interface {
ListSinksWithContext(ctx context.Context, in *oam.ListSinksInput, opts ...request.Option) (*oam.ListSinksOutput, error)
ListAttachedLinksWithContext(ctx context.Context, in *oam.ListAttachedLinksInput, opts ...request.Option) (*oam.ListAttachedLinksOutput, error)
oam.ListSinksAPIClient
oam.ListAttachedLinksAPIClient
}
type EC2APIProvider interface {
DescribeRegionsWithContext(ctx context.Context, in *ec2.DescribeRegionsInput, opts ...request.Option) (*ec2.DescribeRegionsOutput, error)
DescribeInstancesPagesWithContext(ctx context.Context, in *ec2.DescribeInstancesInput, fn func(*ec2.DescribeInstancesOutput, bool) bool, opts ...request.Option) error
DescribeRegions(ctx context.Context, in *ec2.DescribeRegionsInput, optFns ...func(*ec2.Options)) (*ec2.DescribeRegionsOutput, error)
ec2.DescribeInstancesAPIClient
}
type CWLogsClient interface {
StartQuery(context.Context, *cloudwatchlogs.StartQueryInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.StartQueryOutput, error)
StopQuery(context.Context, *cloudwatchlogs.StopQueryInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.StopQueryOutput, error)
GetQueryResults(context.Context, *cloudwatchlogs.GetQueryResultsInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetQueryResultsOutput, error)
cloudwatchlogs.GetLogEventsAPIClient
cloudwatchlogs.DescribeLogGroupsAPIClient
}
type CWClient interface {
AlarmsAPI
cloudwatch.GetMetricDataAPIClient
cloudwatch.ListMetricsAPIClient
}
type AlarmsAPI interface {
cloudwatch.DescribeAlarmsAPIClient
cloudwatch.DescribeAlarmHistoryAPIClient
DescribeAlarmsForMetric(context.Context, *cloudwatch.DescribeAlarmsForMetricInput, ...func(*cloudwatch.Options)) (*cloudwatch.DescribeAlarmsForMetricOutput, error)
}

View File

@ -11,7 +11,8 @@ import (
"strings"
"time"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
"github.com/google/uuid"
"github.com/grafana/grafana-plugin-sdk-go/backend"
@ -74,7 +75,7 @@ type CloudWatchQuery struct {
SqlExpression string
ReturnData bool
Dimensions map[string][]string
Period int
Period int32
Label string
MatchExact bool
UsedExpression string
@ -201,19 +202,23 @@ func (q *CloudWatchQuery) BuildDeepLink(startTime time.Time, endTime time.Time)
return "", fmt.Errorf("could not marshal link: %w", err)
}
url, err := url.Parse(fmt.Sprintf(`https://%s/cloudwatch/deeplink.js`, getEndpoint(q.Region)))
endpoint, err := getEndpoint(q.Region)
if err != nil {
return "", err
}
consoleURL, err := url.Parse(fmt.Sprintf(`https://%s/cloudwatch/deeplink.js`, endpoint))
if err != nil {
return "", fmt.Errorf("unable to parse CloudWatch console deep link")
}
fragment := url.Query()
fragment := consoleURL.Query()
fragment.Set("graph", string(linkProps))
query := url.Query()
query := consoleURL.Query()
query.Set("region", q.Region)
url.RawQuery = query.Encode()
consoleURL.RawQuery = query.Encode()
return fmt.Sprintf(`%s#metricsV2:%s`, url.String(), fragment.Encode()), nil
return fmt.Sprintf(`%s#metricsV2:%s`, consoleURL.String(), fragment.Encode()), nil
}
const timeSeriesQuery = "timeSeriesQuery"
@ -317,10 +322,14 @@ func (q *CloudWatchQuery) validateAndSetDefaults(refId string, metricsDataQuery
}
var err error
q.Period, err = getPeriod(metricsDataQuery, startTime, endTime)
parsedPeriod, err := getPeriod(metricsDataQuery, startTime, endTime)
if err != nil {
return err
}
if parsedPeriod < math.MinInt32 || parsedPeriod > math.MaxInt32 {
return fmt.Errorf("query period doesn't fit int32: %d", parsedPeriod)
}
q.Period = int32(parsedPeriod)
q.Dimensions = map[string][]string{}
if metricsDataQuery.Dimensions != nil {
@ -373,7 +382,7 @@ func (q *CloudWatchQuery) validateAndSetDefaults(refId string, metricsDataQuery
}
}
if q.Region == defaultRegion {
if q.Region == defaultRegion || q.Region == "" {
q.Region = defaultRegionValue
}
@ -503,14 +512,18 @@ func parseDimensions(dimensions dataquery.Dimensions) (map[string][]string, erro
return parsedDimensions, nil
}
func getEndpoint(region string) string {
partition, _ := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), region)
url := defaultConsoleURL
if partition.ID() == endpoints.AwsUsGovPartitionID {
url = usGovConsoleURL
func getEndpoint(region string) (string, error) {
resolver := cloudwatch.NewDefaultEndpointResolver()
endpoint, err := resolver.ResolveEndpoint(region, cloudwatch.EndpointResolverOptions{})
if err != nil {
return "", fmt.Errorf("resolve endpoint failed: %w", err)
}
if partition.ID() == endpoints.AwsCnPartitionID {
url = chinaConsoleURL
consoleURL := defaultConsoleURL
switch endpoint.PartitionID {
case "aws-us-gov":
consoleURL = usGovConsoleURL
case "aws-cn":
consoleURL = chinaConsoleURL
}
return fmt.Sprintf("%s.%s", region, url)
return fmt.Sprintf("%s.%s", region, consoleURL), nil
}

View File

@ -7,13 +7,15 @@ import (
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/kinds/dataquery"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
)
var logger = log.NewNullLogger()
@ -368,7 +370,7 @@ func TestRequestParser(t *testing.T) {
assert.Equal(t, "CPUUtilization", res.MetricName)
assert.Equal(t, "queryref1", res.Id)
assert.Empty(t, res.Expression)
assert.Equal(t, 600, res.Period)
assert.Equal(t, int32(600), res.Period)
assert.True(t, res.ReturnData)
assert.Len(t, res.Dimensions, 2)
assert.Len(t, res.Dimensions["InstanceId"], 1)
@ -411,7 +413,7 @@ func TestRequestParser(t *testing.T) {
assert.Equal(t, "CPUUtilization", res.MetricName)
assert.Equal(t, "queryref1", res.Id)
assert.Empty(t, res.Expression)
assert.Equal(t, 600, res.Period)
assert.Equal(t, int32(600), res.Period)
assert.True(t, res.ReturnData)
assert.Len(t, res.Dimensions, 2)
assert.Len(t, res.Dimensions["InstanceId"], 1)
@ -466,7 +468,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
assert.NoError(t, err)
require.Len(t, res, 1)
require.NotNil(t, res[0])
assert.Equal(t, 900, res[0].Period)
assert.Equal(t, int32(900), res[0].Period)
})
t.Run("Period is parsed correctly if not defined by user", func(t *testing.T) {
@ -497,7 +499,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false)
require.NoError(t, err)
require.Len(t, res, 1)
assert.Equal(t, 60, res[0].Period)
assert.Equal(t, int32(60), res[0].Period)
})
t.Run("Time range is 1 day", func(t *testing.T) {
@ -507,7 +509,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false)
require.NoError(t, err)
require.Len(t, res, 1)
assert.Equal(t, 60, res[0].Period)
assert.Equal(t, int32(60), res[0].Period)
})
t.Run("Time range is 2 days", func(t *testing.T) {
@ -516,7 +518,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false)
require.NoError(t, err)
require.Len(t, res, 1)
assert.Equal(t, 300, res[0].Period)
assert.Equal(t, int32(300), res[0].Period)
})
t.Run("Time range is 7 days", func(t *testing.T) {
@ -526,7 +528,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false)
require.NoError(t, err)
require.Len(t, res, 1)
assert.Equal(t, 900, res[0].Period)
assert.Equal(t, int32(900), res[0].Period)
})
t.Run("Time range is 30 days", func(t *testing.T) {
@ -536,7 +538,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false)
require.NoError(t, err)
require.Len(t, res, 1)
assert.Equal(t, 3600, res[0].Period)
assert.Equal(t, int32(3600), res[0].Period)
})
t.Run("Time range is 90 days", func(t *testing.T) {
@ -546,7 +548,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false)
require.NoError(t, err)
require.Len(t, res, 1)
assert.Equal(t, 21600, res[0].Period)
assert.Equal(t, int32(21600), res[0].Period)
})
t.Run("Time range is 1 year", func(t *testing.T) {
@ -556,7 +558,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false)
require.Nil(t, err)
require.Len(t, res, 1)
assert.Equal(t, 21600, res[0].Period)
assert.Equal(t, int32(21600), res[0].Period)
})
t.Run("Time range is 2 years", func(t *testing.T) {
@ -566,7 +568,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false)
require.NoError(t, err)
require.Len(t, res, 1)
assert.Equal(t, 86400, res[0].Period)
assert.Equal(t, int32(86400), res[0].Period)
})
t.Run("Time range is 2 days, but 16 days ago", func(t *testing.T) {
@ -575,7 +577,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false)
require.NoError(t, err)
require.Len(t, res, 1)
assert.Equal(t, 300, res[0].Period)
assert.Equal(t, int32(300), res[0].Period)
})
t.Run("Time range is 2 days, but 90 days ago", func(t *testing.T) {
@ -584,7 +586,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false)
require.NoError(t, err)
require.Len(t, res, 1)
assert.Equal(t, 3600, res[0].Period)
assert.Equal(t, int32(3600), res[0].Period)
})
t.Run("Time range is 2 days, but 456 days ago", func(t *testing.T) {
@ -593,7 +595,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false)
require.NoError(t, err)
require.Len(t, res, 1)
assert.Equal(t, 21600, res[0].Period)
assert.Equal(t, int32(21600), res[0].Period)
})
})
t.Run("returns error if period is invalid duration", func(t *testing.T) {
@ -624,7 +626,7 @@ func Test_ParseMetricDataQueries_periods(t *testing.T) {
assert.NoError(t, err)
require.Len(t, res, 1)
assert.Equal(t, 9900, res[0].Period)
assert.Equal(t, int32(9900), res[0].Period)
})
}
@ -932,7 +934,6 @@ func Test_migrateAliasToDynamicLabel_single_query_preserves_old_alias_and_create
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
average := "Average"
false := false
queryToMigrate := metricsDataQuery{
CloudWatchMetricsQuery: dataquery.CloudWatchMetricsQuery{
@ -945,7 +946,7 @@ func Test_migrateAliasToDynamicLabel_single_query_preserves_old_alias_and_create
},
Statistic: &average,
Period: utils.Pointer("600"),
Hide: &false,
Hide: aws.Bool(false),
},
}
@ -982,7 +983,7 @@ func Test_ParseMetricDataQueries_migrate_alias_to_label(t *testing.T) {
assert.Equal(t, true, res[0].ReturnData)
assert.Equal(t, "CPUUtilization", res[0].MetricName)
assert.Equal(t, "ec2", res[0].Namespace)
assert.Equal(t, 600, res[0].Period)
assert.Equal(t, int32(600), res[0].Period)
assert.Equal(t, "us-east-1", res[0].Region)
assert.Equal(t, "Average", res[0].Statistic)
})
@ -1031,7 +1032,7 @@ func Test_ParseMetricDataQueries_migrate_alias_to_label(t *testing.T) {
assert.Equal(t, true, res[0].ReturnData)
assert.Equal(t, "CPUUtilization", res[0].MetricName)
assert.Equal(t, "ec2", res[0].Namespace)
assert.Equal(t, 600, res[0].Period)
assert.Equal(t, int32(600), res[0].Period)
assert.Equal(t, "us-east-1", res[0].Region)
assert.Equal(t, "Average", res[0].Statistic)
@ -1041,7 +1042,7 @@ func Test_ParseMetricDataQueries_migrate_alias_to_label(t *testing.T) {
assert.Equal(t, true, res[1].ReturnData)
assert.Equal(t, "CPUUtilization", res[1].MetricName)
assert.Equal(t, "ec2", res[1].Namespace)
assert.Equal(t, 600, res[1].Period)
assert.Equal(t, int32(600), res[1].Period)
assert.Equal(t, "us-east-1", res[1].Region)
assert.Equal(t, "Average", res[1].Statistic)
})
@ -1087,7 +1088,7 @@ func Test_ParseMetricDataQueries_migrate_alias_to_label(t *testing.T) {
assert.Equal(t, true, res[0].ReturnData)
assert.Equal(t, "CPUUtilization", res[0].MetricName)
assert.Equal(t, "ec2", res[0].Namespace)
assert.Equal(t, 600, res[0].Period)
assert.Equal(t, int32(600), res[0].Period)
assert.Equal(t, "us-east-1", res[0].Region)
assert.Equal(t, "Average", res[0].Statistic)
})
@ -1194,7 +1195,7 @@ func Test_ParseMetricDataQueries_account_Id(t *testing.T) {
}
func Test_ParseMetricDataQueries_default_region(t *testing.T) {
t.Run("default region is used when when region not set", func(t *testing.T) {
t.Run("default region is used when when region is default", func(t *testing.T) {
query := []backend.DataQuery{
{
JSON: json.RawMessage(`{
@ -1215,6 +1216,33 @@ func Test_ParseMetricDataQueries_default_region(t *testing.T) {
},
}
region := "us-east-2"
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), region, logger, false)
assert.NoError(t, err)
require.Len(t, res, 1)
require.NotNil(t, res[0])
assert.Equal(t, region, res[0].Region)
})
t.Run("default region is used when when region not set", func(t *testing.T) {
query := []backend.DataQuery{
{
JSON: json.RawMessage(`{
"refId":"ref1",
"namespace":"ec2",
"metricName":"CPUUtilization",
"id": "",
"expression": "",
"dimensions":{
"InstanceId":["test"],
"InstanceType":["test2"]
},
"statistic":"Average",
"period":"900",
"hide":false
}`),
},
}
region := "us-east-2"
res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), region, logger, false)
assert.NoError(t, err)
@ -1305,7 +1333,8 @@ func TestGetEndpoint(t *testing.T) {
}
for _, ts := range testcases {
t.Run(fmt.Sprintf("should create correct endpoint for %s", ts), func(t *testing.T) {
actual := getEndpoint(ts.region)
actual, err := getEndpoint(ts.region)
assert.NoError(t, err)
assert.Equal(t, ts.expectedEndpoint, actual)
})
}

View File

@ -8,7 +8,7 @@ type LogsQuery struct {
dataquery.CloudWatchLogsQuery
StartTime *int64
EndTime *int64
Limit *int64
Limit *int32
LogGroupName string
LogStreamName string
QueryId string

View File

@ -1,32 +1,32 @@
package models
import (
"github.com/aws/aws-sdk-go/service/cloudwatch"
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
)
// queryRowResponse represents the GetMetricData response for a query row in the query editor.
// QueryRowResponse represents the GetMetricData response for a query row in the query editor.
type QueryRowResponse struct {
partialDataSet map[string]*cloudwatch.MetricDataResult
partialDataSet map[string]*cloudwatchtypes.MetricDataResult
ErrorCodes map[string]bool
HasArithmeticError bool
ArithmeticErrorMessage string
HasPermissionError bool
PermissionErrorMessage string
Metrics []*cloudwatch.MetricDataResult
StatusCode string
Metrics []*cloudwatchtypes.MetricDataResult
StatusCode cloudwatchtypes.StatusCode
}
func NewQueryRowResponse(errors map[string]bool) QueryRowResponse {
return QueryRowResponse{
partialDataSet: make(map[string]*cloudwatch.MetricDataResult),
partialDataSet: make(map[string]*cloudwatchtypes.MetricDataResult),
ErrorCodes: errors,
HasArithmeticError: false,
ArithmeticErrorMessage: "",
Metrics: []*cloudwatch.MetricDataResult{},
Metrics: []*cloudwatchtypes.MetricDataResult{},
}
}
func (q *QueryRowResponse) AddMetricDataResult(mdr *cloudwatch.MetricDataResult) {
func (q *QueryRowResponse) AddMetricDataResult(mdr *cloudwatchtypes.MetricDataResult) {
if mdr.Label == nil {
return
}
@ -34,16 +34,16 @@ func (q *QueryRowResponse) AddMetricDataResult(mdr *cloudwatch.MetricDataResult)
if partialData, ok := q.partialDataSet[*mdr.Label]; ok {
partialData.Timestamps = append(partialData.Timestamps, mdr.Timestamps...)
partialData.Values = append(partialData.Values, mdr.Values...)
q.StatusCode = *mdr.StatusCode
if *mdr.StatusCode != "PartialData" {
q.StatusCode = mdr.StatusCode
if mdr.StatusCode != cloudwatchtypes.StatusCodePartialData {
delete(q.partialDataSet, *mdr.Label)
}
return
}
q.Metrics = append(q.Metrics, mdr)
q.StatusCode = *mdr.StatusCode
if *mdr.StatusCode == "PartialData" {
q.StatusCode = mdr.StatusCode
if mdr.StatusCode == cloudwatchtypes.StatusCodePartialData {
q.partialDataSet[*mdr.Label] = mdr
}
}

View File

@ -6,11 +6,11 @@ import (
"strconv"
)
const defaultLogGroupLimit = int64(50)
const defaultLogGroupLimit = int32(50)
type LogGroupsRequest struct {
ResourceRequest
Limit int64
Limit int32
LogGroupNamePrefix, LogGroupNamePattern *string
ListAllLogGroups bool
}
@ -45,11 +45,11 @@ func setIfNotEmptyString(paramValue string) *string {
return &paramValue
}
func getLimit(limit string) int64 {
func getLimit(limit string) int32 {
logGroupLimit := defaultLogGroupLimit
intLimit, err := strconv.ParseInt(limit, 10, 64)
intLimit, err := strconv.ParseInt(limit, 10, 32)
if err == nil && intLimit > 0 {
logGroupLimit = intLimit
logGroupLimit = int32(intLimit)
}
return logGroupLimit
}

View File

@ -1,6 +1,8 @@
package resources
import "github.com/aws/aws-sdk-go/service/cloudwatch"
import (
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
)
type Dimension struct {
Name string
@ -13,7 +15,7 @@ type ResourceResponse[T any] struct {
}
type MetricResponse struct {
*cloudwatch.Metric
Metric cloudwatchtypes.Metric
AccountId *string `json:"accountId,omitempty"`
}

View File

@ -8,7 +8,6 @@ import (
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource"
)
type Duration struct {
@ -67,11 +66,11 @@ func (duration *Duration) UnmarshalJSON(b []byte) error {
}
dur, err := time.ParseDuration(value)
if err != nil {
return errorsource.DownstreamError(err, false)
return backend.DownstreamError(err)
}
*duration = Duration{dur}
default:
return errorsource.DownstreamError(fmt.Errorf("invalid duration: %#v", unmarshalledJson), false)
return backend.DownstreamError(fmt.Errorf("invalid duration: %#v", unmarshalledJson))
}
return nil

View File

@ -17,7 +17,7 @@ type metricExpression struct {
type metricStatMeta struct {
Stat string `json:"stat"`
Period int `json:"period"`
Period int32 `json:"period"`
Label string `json:"label,omitempty"`
AccountId string `json:"accountId,omitempty"`
}

View File

@ -1,29 +1,17 @@
package routes
package cloudwatch
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
)
func Test_Namespaces_Route(t *testing.T) {
customNamespaces := ""
factoryFunc := func(_ context.Context, pluginCtx backend.PluginContext, region string) (reqCtx models.RequestContext, err error) {
return models.RequestContext{
Settings: models.CloudWatchSettings{
Namespace: customNamespaces,
},
}, nil
}
t.Run("calls GetHardCodedNamespaces", func(t *testing.T) {
origGetHardCodedNamespaces := services.GetHardCodedNamespaces
t.Cleanup(func() {
@ -36,7 +24,8 @@ func Test_Namespaces_Route(t *testing.T) {
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/namespaces", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(NamespacesHandler, logger, factoryFunc))
ds := newTestDatasource()
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.NamespacesHandler))
handler.ServeHTTP(rr, req)
assert.True(t, haveBeenCalled)
})
@ -51,8 +40,10 @@ func Test_Namespaces_Route(t *testing.T) {
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/namespaces", nil)
customNamespaces = "customNamespace1,customNamespace2"
handler := http.HandlerFunc(ResourceRequestMiddleware(NamespacesHandler, logger, factoryFunc))
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.Namespace = "customNamespace1,customNamespace2"
})
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.NamespacesHandler))
handler.ServeHTTP(rr, req)
assert.JSONEq(t, `[{"value":"AWS/EC2"}, {"value":"AWS/ELB"}, {"value":"customNamespace1"}, {"value":"customNamespace2"}]`, rr.Body.String())
})
@ -67,8 +58,10 @@ func Test_Namespaces_Route(t *testing.T) {
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/namespaces", nil)
customNamespaces = "DCustomNamespace1,ACustomNamespace2"
handler := http.HandlerFunc(ResourceRequestMiddleware(NamespacesHandler, logger, factoryFunc))
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.Namespace = "DCustomNamespace1,ACustomNamespace2"
})
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.NamespacesHandler))
handler.ServeHTTP(rr, req)
assert.JSONEq(t, `[{"value":"ACustomNamespace2"}, {"value":"AWS/ELB"}, {"value":"AWS/XYZ"}, {"value":"DCustomNamespace1"}]`, rr.Body.String())
})

View File

@ -0,0 +1,76 @@
package cloudwatch
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestRegionsRoute(t *testing.T) {
origNewRegionsService := services.NewRegionsService
t.Cleanup(func() {
services.NewRegionsService = origNewRegionsService
})
var mockRegionService mocks.RegionsService
services.NewRegionsService = func(models.EC2APIProvider, log.Logger) models.RegionsAPIProvider {
return &mockRegionService
}
t.Run("returns 200 and regions", func(t *testing.T) {
mockRegionService = mocks.RegionsService{}
mockRegionService.On("GetRegions", mock.Anything).Return([]resources.ResourceResponse[resources.Region]{{
Value: resources.Region{
Name: "us-east-1",
},
}}, nil).Once()
rr := httptest.NewRecorder()
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.Region = "us-east-1"
})
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.RegionsHandler))
req := httptest.NewRequest("GET", `/regions`, nil)
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "us-east-1")
})
t.Run("returns 400 when the service returns a missing region error", func(t *testing.T) {
rr := httptest.NewRecorder()
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.Region = ""
})
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.RegionsHandler))
req := httptest.NewRequest("GET", `/regions`, nil)
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Error in Regions Handler when connecting to aws without a default region selection: missing default region")
})
t.Run("returns 500 when get regions returns an error", func(t *testing.T) {
mockRegionService = mocks.RegionsService{}
mockRegionService.On("GetRegions", mock.Anything).Return([]resources.ResourceResponse[resources.Region](nil), errors.New("aws is having some kind of outage")).Once()
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", `/regions`, nil)
ds := newTestDatasource(func(ds *DataSource) {
ds.Settings.Region = "us-east-1"
})
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.RegionsHandler))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
assert.Contains(t, rr.Body.String(), "Error in Regions Handler while fetching regions: aws is having some kind of outage")
})
}

View File

@ -3,60 +3,67 @@ package cloudwatch
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"sort"
"strings"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/routes"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/clients"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
)
func (e *cloudWatchExecutor) newResourceMux() *http.ServeMux {
func (ds *DataSource) newResourceMux() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/ebs-volume-ids", handleResourceReq(e.handleGetEbsVolumeIds, e.logger))
mux.HandleFunc("/ec2-instance-attribute", handleResourceReq(e.handleGetEc2InstanceAttribute, e.logger))
mux.HandleFunc("/resource-arns", handleResourceReq(e.handleGetResourceArns, e.logger))
mux.HandleFunc("/log-groups", routes.ResourceRequestMiddleware(routes.LogGroupsHandler, e.logger, e.getRequestContext))
mux.HandleFunc("/metrics", routes.ResourceRequestMiddleware(routes.MetricsHandler, e.logger, e.getRequestContext))
mux.HandleFunc("/dimension-values", routes.ResourceRequestMiddleware(routes.DimensionValuesHandler, e.logger, e.getRequestContext))
mux.HandleFunc("/dimension-keys", routes.ResourceRequestMiddleware(routes.DimensionKeysHandler, e.logger, e.getRequestContext))
mux.HandleFunc("/accounts", routes.ResourceRequestMiddleware(routes.AccountsHandler, e.logger, e.getRequestContext))
mux.HandleFunc("/namespaces", routes.ResourceRequestMiddleware(routes.NamespacesHandler, e.logger, e.getRequestContext))
mux.HandleFunc("/log-group-fields", routes.ResourceRequestMiddleware(routes.LogGroupFieldsHandler, e.logger, e.getRequestContext))
mux.HandleFunc("/external-id", routes.ResourceRequestMiddleware(routes.ExternalIdHandler, e.logger, e.getRequestContextOnlySettings))
mux.HandleFunc("/regions", routes.ResourceRequestMiddleware(routes.RegionsHandler, e.logger, e.getRequestContext))
mux.HandleFunc("/ebs-volume-ids", ds.handleResourceReq(ds.handleGetEbsVolumeIds))
mux.HandleFunc("/ec2-instance-attribute", ds.handleResourceReq(ds.handleGetEc2InstanceAttribute))
mux.HandleFunc("/resource-arns", ds.handleResourceReq(ds.handleGetResourceArns))
mux.HandleFunc("/log-groups", ds.resourceRequestMiddleware(ds.LogGroupsHandler))
mux.HandleFunc("/metrics", ds.resourceRequestMiddleware(ds.MetricsHandler))
mux.HandleFunc("/dimension-values", ds.resourceRequestMiddleware(ds.DimensionValuesHandler))
mux.HandleFunc("/dimension-keys", ds.resourceRequestMiddleware(ds.DimensionKeysHandler))
mux.HandleFunc("/accounts", ds.resourceRequestMiddleware(ds.AccountsHandler))
mux.HandleFunc("/namespaces", ds.resourceRequestMiddleware(ds.NamespacesHandler))
mux.HandleFunc("/log-group-fields", ds.resourceRequestMiddleware(ds.LogGroupFieldsHandler))
mux.HandleFunc("/external-id", ds.resourceRequestMiddleware(ds.ExternalIdHandler))
mux.HandleFunc("/regions", ds.resourceRequestMiddleware(ds.RegionsHandler))
// remove this once AWS's Cross Account Observability is supported in GovCloud
mux.HandleFunc("/legacy-log-groups", handleResourceReq(e.handleGetLogGroups, e.logger))
mux.HandleFunc("/legacy-log-groups", ds.handleResourceReq(ds.handleGetLogGroups))
return mux
}
type handleFn func(ctx context.Context, pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error)
type handleFn func(ctx context.Context, parameters url.Values) ([]suggestData, error)
func handleResourceReq(handleFunc handleFn, logger log.Logger) func(rw http.ResponseWriter, req *http.Request) {
// TODO: merge this and resourceRequestMiddleware
func (ds *DataSource) handleResourceReq(handleFunc handleFn) func(rw http.ResponseWriter, req *http.Request) {
return func(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
pluginContext := backend.PluginConfigFromContext(ctx)
logger := ds.logger.FromContext(ctx)
err := req.ParseForm()
if err != nil {
writeResponse(rw, http.StatusBadRequest, fmt.Sprintf("unexpected error %v", err), logger.FromContext(ctx))
writeResponse(rw, http.StatusBadRequest, fmt.Sprintf("unexpected error %v", err), logger)
return
}
data, err := handleFunc(ctx, pluginContext, req.URL.Query())
data, err := handleFunc(ctx, req.URL.Query())
if err != nil {
writeResponse(rw, http.StatusBadRequest, fmt.Sprintf("unexpected error %v", err), logger.FromContext(ctx))
writeResponse(rw, http.StatusBadRequest, fmt.Sprintf("unexpected error %v", err), logger)
return
}
body, err := json.Marshal(data)
if err != nil {
writeResponse(rw, http.StatusBadRequest, fmt.Sprintf("unexpected error %v", err), logger.FromContext(ctx))
writeResponse(rw, http.StatusBadRequest, fmt.Sprintf("unexpected error %v", err), logger)
return
}
rw.WriteHeader(http.StatusOK)
_, err = rw.Write(body)
if err != nil {
logger.FromContext(ctx).Error("Unable to write HTTP response", "error", err)
ds.logger.Error("Unable to write HTTP response", "error", err)
return
}
}
@ -69,3 +76,292 @@ func writeResponse(rw http.ResponseWriter, code int, msg string, logger log.Logg
logger.Error("Unable to write HTTP response", "error", err)
}
}
func (ds *DataSource) LogGroupsHandler(ctx context.Context, parameters url.Values) ([]byte, *models.HttpError) {
request, err := resources.ParseLogGroupsRequest(parameters)
if err != nil {
return nil, models.NewHttpError("cannot set both log group name prefix and pattern", http.StatusBadRequest, err)
}
service, err := ds.GetLogGroupsService(ctx, request.Region)
if err != nil {
return nil, models.NewHttpError("GetLogGroupsService error", http.StatusInternalServerError, err)
}
logGroups, err := service.GetLogGroups(ctx, request)
if err != nil {
return nil, models.NewHttpError("GetLogGroups error", http.StatusInternalServerError, err)
}
logGroupsResponse, err := json.Marshal(logGroups)
if err != nil {
return nil, models.NewHttpError("LogGroupsHandler json error", http.StatusInternalServerError, err)
}
return logGroupsResponse, nil
}
func (ds *DataSource) MetricsHandler(ctx context.Context, parameters url.Values) ([]byte, *models.HttpError) {
metricsRequest, err := resources.GetMetricsRequest(parameters)
if err != nil {
return nil, models.NewHttpError("error in MetricsHandler", http.StatusBadRequest, err)
}
service, err := ds.GetListMetricsService(ctx, metricsRequest.Region)
if err != nil {
return nil, models.NewHttpError("error in MetricsHandler", http.StatusInternalServerError, err)
}
var response []resources.ResourceResponse[resources.Metric]
switch metricsRequest.Type() {
case resources.AllMetricsRequestType:
response = services.GetAllHardCodedMetrics()
case resources.MetricsByNamespaceRequestType:
response, err = services.GetHardCodedMetricsByNamespace(metricsRequest.Namespace)
case resources.CustomNamespaceRequestType:
response, err = service.GetMetricsByNamespace(ctx, metricsRequest)
}
if err != nil {
return nil, models.NewHttpError("error in MetricsHandler", http.StatusInternalServerError, err)
}
metricsResponse, err := json.Marshal(response)
if err != nil {
return nil, models.NewHttpError("error in MetricsHandler", http.StatusInternalServerError, err)
}
return metricsResponse, nil
}
func (ds *DataSource) DimensionValuesHandler(ctx context.Context, parameters url.Values) ([]byte, *models.HttpError) {
dimensionValuesRequest, err := resources.GetDimensionValuesRequest(parameters)
if err != nil {
return nil, models.NewHttpError("error in DimensionValuesHandler", http.StatusBadRequest, err)
}
service, err := ds.GetListMetricsService(ctx, dimensionValuesRequest.Region)
if err != nil {
return nil, models.NewHttpError("error in DimensionValuesHandler", http.StatusInternalServerError, err)
}
response, err := service.GetDimensionValuesByDimensionFilter(ctx, dimensionValuesRequest)
if err != nil {
return nil, models.NewHttpError("error in DimensionValuesHandler", http.StatusInternalServerError, err)
}
dimensionValuesResponse, err := json.Marshal(response)
if err != nil {
return nil, models.NewHttpError("error in DimensionValuesHandler", http.StatusInternalServerError, err)
}
return dimensionValuesResponse, nil
}
func (ds *DataSource) DimensionKeysHandler(ctx context.Context, parameters url.Values) ([]byte, *models.HttpError) {
dimensionKeysRequest, err := resources.GetDimensionKeysRequest(parameters)
if err != nil {
return nil, models.NewHttpError("error in DimensionKeyHandler", http.StatusBadRequest, err)
}
service, err := ds.GetListMetricsService(ctx, dimensionKeysRequest.Region)
if err != nil {
return nil, models.NewHttpError("error in DimensionKeyHandler", http.StatusInternalServerError, err)
}
var response []resources.ResourceResponse[string]
switch dimensionKeysRequest.Type() {
case resources.FilterDimensionKeysRequest:
response, err = service.GetDimensionKeysByDimensionFilter(ctx, dimensionKeysRequest)
default:
response, err = services.GetHardCodedDimensionKeysByNamespace(dimensionKeysRequest.Namespace)
}
if err != nil {
return nil, models.NewHttpError("error in DimensionKeyHandler", http.StatusInternalServerError, err)
}
jsonResponse, err := json.Marshal(response)
if err != nil {
return nil, models.NewHttpError("error in DimensionKeyHandler", http.StatusInternalServerError, err)
}
return jsonResponse, nil
}
func (ds *DataSource) AccountsHandler(ctx context.Context, parameters url.Values) ([]byte, *models.HttpError) {
region := parameters.Get("region")
if region == "" {
return nil, models.NewHttpError("error in AccountsHandler", http.StatusBadRequest, fmt.Errorf("region is required"))
}
service, err := ds.GetAccountsService(ctx, region)
if err != nil {
return nil, models.NewHttpError("error in AccountsHandler", http.StatusInternalServerError, err)
}
accounts, err := service.GetAccountsForCurrentUserOrRole(ctx)
if err != nil {
msg := "error getting accounts for current user or role"
switch {
case errors.Is(err, services.ErrAccessDeniedException):
return nil, models.NewHttpError(msg, http.StatusForbidden, err)
default:
return nil, models.NewHttpError(msg, http.StatusInternalServerError, err)
}
}
accountsResponse, err := json.Marshal(accounts)
if err != nil {
return nil, models.NewHttpError("error in AccountsHandler", http.StatusInternalServerError, err)
}
return accountsResponse, nil
}
func (ds *DataSource) NamespacesHandler(_ context.Context, _ url.Values) ([]byte, *models.HttpError) {
response := services.GetHardCodedNamespaces()
customNamespace := ds.Settings.Namespace
if customNamespace != "" {
customNamespaces := strings.Split(customNamespace, ",")
for _, customNamespace := range customNamespaces {
response = append(response, resources.ResourceResponse[string]{Value: customNamespace})
}
}
sort.Slice(response, func(i, j int) bool {
return response[i].Value < response[j].Value
})
namespacesResponse, err := json.Marshal(response)
if err != nil {
return nil, models.NewHttpError("error in NamespacesHandler", http.StatusInternalServerError, err)
}
return namespacesResponse, nil
}
func (ds *DataSource) LogGroupFieldsHandler(ctx context.Context, parameters url.Values) ([]byte, *models.HttpError) {
request, err := resources.ParseLogGroupFieldsRequest(parameters)
if err != nil {
return nil, models.NewHttpError("error in LogGroupFieldsHandler", http.StatusBadRequest, err)
}
service, err := ds.GetLogGroupsService(ctx, request.Region)
if err != nil {
return nil, models.NewHttpError("newLogGroupsService error", http.StatusInternalServerError, err)
}
logGroupFields, err := service.GetLogGroupFields(ctx, request)
if err != nil {
return nil, models.NewHttpError("GetLogGroupFields error", http.StatusInternalServerError, err)
}
logGroupsResponse, err := json.Marshal(logGroupFields)
if err != nil {
return nil, models.NewHttpError("LogGroupFieldsHandler json error", http.StatusInternalServerError, err)
}
return logGroupsResponse, nil
}
func (ds *DataSource) ExternalIdHandler(_ context.Context, _ url.Values) ([]byte, *models.HttpError) {
response := map[string]string{
"externalId": ds.Settings.GrafanaSettings.ExternalID,
}
jsonResponse, err := json.Marshal(response)
if err != nil {
return nil, models.NewHttpError("error in ExternalIdHandler", http.StatusInternalServerError, err)
}
return jsonResponse, nil
}
func (ds *DataSource) RegionsHandler(ctx context.Context, _ url.Values) ([]byte, *models.HttpError) {
service, err := ds.GetRegionsService(ctx, defaultRegion)
if err != nil {
if errors.Is(err, models.ErrMissingRegion) {
return nil, models.NewHttpError("Error in Regions Handler when connecting to aws without a default region selection", http.StatusBadRequest, err)
}
return nil, models.NewHttpError("Error in Regions Handler when connecting to aws", http.StatusInternalServerError, err)
}
regions, err := service.GetRegions(ctx)
if err != nil {
return nil, models.NewHttpError("Error in Regions Handler while fetching regions", http.StatusInternalServerError, err)
}
regionsResponse, err := json.Marshal(regions)
if err != nil {
return nil, models.NewHttpError("Error in Regions Handler while parsing regions", http.StatusInternalServerError, err)
}
return regionsResponse, nil
}
func (ds *DataSource) GetLogGroupsService(ctx context.Context, region string) (models.LogGroupsProvider, error) {
awsConfig, err := ds.newAWSConfig(ctx, region)
if err != nil {
return nil, err
}
return services.NewLogGroupsService(NewLogsAPI(awsConfig), features.IsEnabled(ctx, features.FlagCloudWatchCrossAccountQuerying)), nil
}
func (ds *DataSource) GetListMetricsService(ctx context.Context, region string) (models.ListMetricsProvider, error) {
awsConfig, err := ds.newAWSConfig(ctx, region)
if err != nil {
return nil, err
}
return services.NewListMetricsService(clients.NewMetricsClient(NewCWClient(awsConfig), ds.Settings.GrafanaSettings.ListMetricsPageLimit)), nil
}
func (ds *DataSource) GetAccountsService(ctx context.Context, region string) (models.AccountsProvider, error) {
awsCfg, err := ds.newAWSConfig(ctx, region)
if err != nil {
return nil, err
}
return services.NewAccountsService(NewOAMAPI(awsCfg)), nil
}
func (ds *DataSource) GetRegionsService(ctx context.Context, region string) (models.RegionsAPIProvider, error) {
awsCfg, err := ds.newAWSConfig(ctx, region)
if err != nil {
return nil, err
}
return services.NewRegionsService(NewEC2API(awsCfg), ds.logger), nil
}
// TODO: merge this and handleResourceReq
func (ds *DataSource) resourceRequestMiddleware(handleFunc models.RouteHandlerFunc) func(rw http.ResponseWriter, req *http.Request) {
return func(rw http.ResponseWriter, req *http.Request) {
if req.Method != "GET" {
respondWithError(rw, models.NewHttpError("Invalid method", http.StatusMethodNotAllowed, nil))
return
}
ctx := req.Context()
jsonResponse, httpError := handleFunc(ctx, req.URL.Query())
if httpError != nil {
ds.logger.FromContext(ctx).Error("Error handling resource request", "error", httpError.Message)
respondWithError(rw, httpError)
return
}
rw.Header().Set("Content-Type", "application/json")
_, err := rw.Write(jsonResponse)
if err != nil {
ds.logger.FromContext(ctx).Error("Error handling resource request", "error", err)
respondWithError(rw, models.NewHttpError("error writing response in resource request middleware", http.StatusInternalServerError, err))
}
}
}
func respondWithError(rw http.ResponseWriter, httpError *models.HttpError) {
response, err := json.Marshal(httpError)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(httpError.StatusCode)
_, err = rw.Write(response)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
}
}

View File

@ -8,7 +8,8 @@ import (
"strings"
"time"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
@ -18,7 +19,7 @@ import (
// matches a dynamic label
var dynamicLabel = regexp.MustCompile(`\$\{.+\}`)
func (e *cloudWatchExecutor) parseResponse(ctx context.Context, metricDataOutputs []*cloudwatch.GetMetricDataOutput,
func (ds *DataSource) parseResponse(ctx context.Context, metricDataOutputs []*cloudwatch.GetMetricDataOutput,
queries []*models.CloudWatchQuery) ([]*responseWrapper, error) {
aggregatedResponse := aggregateResponse(metricDataOutputs)
queriesById := map[string]*models.CloudWatchQuery{}
@ -88,7 +89,7 @@ func aggregateResponse(getMetricDataOutputs []*cloudwatch.GetMetricDataOutput) m
}
}
response.AddMetricDataResult(r)
response.AddMetricDataResult(&r)
responseByID[id] = response
}
}
@ -228,16 +229,9 @@ func buildDataFrames(ctx context.Context, aggregatedResponse models.QueryRowResp
} else {
labels = getLabels(label, query, false)
}
timestamps := []*time.Time{}
points := []*float64{}
for j, t := range metric.Timestamps {
val := metric.Values[j]
timestamps = append(timestamps, t)
points = append(points, val)
}
timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, timestamps)
valueField := data.NewField(data.TimeSeriesValueFieldName, labels, points)
timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, metric.Timestamps)
valueField := data.NewField(data.TimeSeriesValueFieldName, labels, metric.Values)
// CloudWatch appends the dimensions to the returned label if the query label is not dynamic, so static labels need to be set
if hasStaticLabel {

View File

@ -7,8 +7,10 @@ import (
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/stretchr/testify/assert"
@ -41,7 +43,7 @@ func TestCloudWatchResponseParser(t *testing.T) {
assert.Len(t, aggregatedResponse[idA].Metrics[0].Values, 10)
})
t.Run("should have statuscode 'Complete'", func(t *testing.T) {
assert.Equal(t, "Complete", aggregatedResponse[idA].StatusCode)
assert.Equal(t, cloudwatchtypes.StatusCodeComplete, aggregatedResponse[idA].StatusCode)
})
t.Run("should have exceeded request limit", func(t *testing.T) {
assert.True(t, aggregatedResponse[idA].ErrorCodes["MaxMetricsExceeded"])
@ -63,7 +65,7 @@ func TestCloudWatchResponseParser(t *testing.T) {
aggregatedResponse := aggregateResponse(getMetricDataOutputs)
idB := "b"
t.Run("should have statuscode is 'PartialData'", func(t *testing.T) {
assert.Equal(t, "PartialData", aggregatedResponse[idB].StatusCode)
assert.Equal(t, cloudwatchtypes.StatusCodePartialData, aggregatedResponse[idB].StatusCode)
})
t.Run("should have an arithmetic error and an error message", func(t *testing.T) {
assert.True(t, aggregatedResponse[idB].HasArithmeticError)
@ -85,7 +87,7 @@ func TestCloudWatchResponseParser(t *testing.T) {
assert.Len(t, aggregatedResponse[idA].Metrics[0].Values, 6)
})
t.Run("should have statuscode 'Complete'", func(t *testing.T) {
assert.Equal(t, "Complete", aggregatedResponse[idA].StatusCode)
assert.Equal(t, cloudwatchtypes.StatusCodeComplete, aggregatedResponse[idA].StatusCode)
})
})
@ -153,36 +155,36 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
t.Run("using multi filter", func(t *testing.T) {
timestamp := time.Unix(0, 0)
response := &models.QueryRowResponse{
Metrics: []*cloudwatch.MetricDataResult{
Metrics: []*cloudwatchtypes.MetricDataResult{
{
Id: aws.String("id1"),
Label: aws.String("lb1|&|lb1"),
Timestamps: []*time.Time{
aws.Time(timestamp),
aws.Time(timestamp.Add(time.Minute)),
aws.Time(timestamp.Add(3 * time.Minute)),
Timestamps: []time.Time{
timestamp,
timestamp.Add(time.Minute),
timestamp.Add(3 * time.Minute),
},
Values: []*float64{
aws.Float64(10),
aws.Float64(20),
aws.Float64(30),
Values: []float64{
10,
20,
30,
},
StatusCode: aws.String("Complete"),
StatusCode: cloudwatchtypes.StatusCodeComplete,
},
{
Id: aws.String("id2"),
Label: aws.String("lb2|&|lb2"),
Timestamps: []*time.Time{
aws.Time(timestamp),
aws.Time(timestamp.Add(time.Minute)),
aws.Time(timestamp.Add(3 * time.Minute)),
Timestamps: []time.Time{
timestamp,
timestamp.Add(time.Minute),
timestamp.Add(3 * time.Minute),
},
Values: []*float64{
aws.Float64(10),
aws.Float64(20),
aws.Float64(30),
Values: []float64{
10,
20,
30,
},
StatusCode: aws.String("Complete"),
StatusCode: cloudwatchtypes.StatusCodeComplete,
},
},
}
@ -223,36 +225,36 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
t.Run("using multiple wildcard filters", func(t *testing.T) {
timestamp := time.Unix(0, 0)
response := &models.QueryRowResponse{
Metrics: []*cloudwatch.MetricDataResult{
Metrics: []*cloudwatchtypes.MetricDataResult{
{
Id: aws.String("lb3"),
Label: aws.String("some label lb3|&|inst1|&|balancer 1"),
Timestamps: []*time.Time{
aws.Time(timestamp),
aws.Time(timestamp.Add(time.Minute)),
aws.Time(timestamp.Add(3 * time.Minute)),
Timestamps: []time.Time{
timestamp,
timestamp.Add(time.Minute),
timestamp.Add(3 * time.Minute),
},
Values: []*float64{
aws.Float64(10),
aws.Float64(20),
aws.Float64(30),
Values: []float64{
10,
20,
30,
},
StatusCode: aws.String("Complete"),
StatusCode: cloudwatchtypes.StatusCodeComplete,
},
{
Id: aws.String("lb4"),
Label: aws.String("some label lb4|&|inst2|&|balancer 2"),
Timestamps: []*time.Time{
aws.Time(timestamp),
aws.Time(timestamp.Add(time.Minute)),
aws.Time(timestamp.Add(3 * time.Minute)),
Timestamps: []time.Time{
timestamp,
timestamp.Add(time.Minute),
timestamp.Add(3 * time.Minute),
},
Values: []*float64{
aws.Float64(10),
aws.Float64(20),
aws.Float64(30),
Values: []float64{
10,
20,
30,
},
StatusCode: aws.String("Complete"),
StatusCode: cloudwatchtypes.StatusCodeComplete,
},
},
}
@ -294,17 +296,17 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
timestamp := time.Unix(0, 0)
// When there are no results, CloudWatch sets the label values to --
response := &models.QueryRowResponse{
Metrics: []*cloudwatch.MetricDataResult{
Metrics: []*cloudwatchtypes.MetricDataResult{
{
Id: aws.String("lb3"),
Label: aws.String("some label|&|--"),
Timestamps: []*time.Time{
aws.Time(timestamp),
aws.Time(timestamp.Add(time.Minute)),
aws.Time(timestamp.Add(3 * time.Minute)),
Timestamps: []time.Time{
timestamp,
timestamp.Add(time.Minute),
timestamp.Add(3 * time.Minute),
},
Values: []*float64{},
StatusCode: aws.String("Complete"),
Values: []float64{},
StatusCode: cloudwatchtypes.StatusCodeComplete,
},
},
}
@ -337,17 +339,17 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
timestamp := time.Unix(0, 0)
// When there are no results, CloudWatch sets the label values to --
response := &models.QueryRowResponse{
Metrics: []*cloudwatch.MetricDataResult{
Metrics: []*cloudwatchtypes.MetricDataResult{
{
Id: aws.String("lb3"),
Label: aws.String("some label|&|--"),
Timestamps: []*time.Time{
aws.Time(timestamp),
aws.Time(timestamp.Add(time.Minute)),
aws.Time(timestamp.Add(3 * time.Minute)),
Timestamps: []time.Time{
timestamp,
timestamp.Add(time.Minute),
timestamp.Add(3 * time.Minute),
},
Values: []*float64{},
StatusCode: aws.String("Complete"),
Values: []float64{},
StatusCode: cloudwatchtypes.StatusCodeComplete,
},
},
}
@ -387,15 +389,15 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
t.Run("when not using multi-value dimension filters on a `MetricSearch` query", func(t *testing.T) {
timestamp := time.Unix(0, 0)
response := &models.QueryRowResponse{
Metrics: []*cloudwatch.MetricDataResult{
Metrics: []*cloudwatchtypes.MetricDataResult{
{
Id: aws.String("lb3"),
Label: aws.String("some label"),
Timestamps: []*time.Time{
aws.Time(timestamp),
Timestamps: []time.Time{
timestamp,
},
Values: []*float64{aws.Float64(23)},
StatusCode: aws.String("Complete"),
Values: []float64{23},
StatusCode: cloudwatchtypes.StatusCodeComplete,
},
},
}
@ -429,15 +431,15 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
t.Run("when non-static label set on a `MetricSearch` query", func(t *testing.T) {
timestamp := time.Unix(0, 0)
response := &models.QueryRowResponse{
Metrics: []*cloudwatch.MetricDataResult{
Metrics: []*cloudwatchtypes.MetricDataResult{
{
Id: aws.String("lb3"),
Label: aws.String("some label|&|res"),
Timestamps: []*time.Time{
aws.Time(timestamp),
Timestamps: []time.Time{
timestamp,
},
Values: []*float64{aws.Float64(23)},
StatusCode: aws.String("Complete"),
Values: []float64{23},
StatusCode: cloudwatchtypes.StatusCodeComplete,
},
},
}
@ -472,15 +474,15 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
t.Run("when static label set on a `MetricSearch` query", func(t *testing.T) {
timestamp := time.Unix(0, 0)
response := &models.QueryRowResponse{
Metrics: []*cloudwatch.MetricDataResult{
Metrics: []*cloudwatchtypes.MetricDataResult{
{
Id: aws.String("lb3"),
Label: aws.String("some label|&|res"),
Timestamps: []*time.Time{
aws.Time(timestamp),
Timestamps: []time.Time{
timestamp,
},
Values: []*float64{aws.Float64(23)},
StatusCode: aws.String("Complete"),
Values: []float64{23},
StatusCode: cloudwatchtypes.StatusCodeComplete,
},
},
}
@ -515,15 +517,15 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
t.Run("when code editor used for `MetricSearch` query add fallback label", func(t *testing.T) {
timestamp := time.Unix(0, 0)
response := &models.QueryRowResponse{
Metrics: []*cloudwatch.MetricDataResult{
Metrics: []*cloudwatchtypes.MetricDataResult{
{
Id: aws.String("lb3"),
Label: aws.String("some label"),
Timestamps: []*time.Time{
aws.Time(timestamp),
Timestamps: []time.Time{
timestamp,
},
Values: []*float64{aws.Float64(23)},
StatusCode: aws.String("Complete"),
Values: []float64{23},
StatusCode: cloudwatchtypes.StatusCodeComplete,
},
},
}
@ -553,24 +555,24 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
t.Run("when `MetricQuery` query has no label set and `GROUP BY` clause has multiple fields", func(t *testing.T) {
timestamp := time.Unix(0, 0)
response := &models.QueryRowResponse{
Metrics: []*cloudwatch.MetricDataResult{
Metrics: []*cloudwatchtypes.MetricDataResult{
{
Id: aws.String("query1"),
Label: aws.String("EC2 vCPU"),
Timestamps: []*time.Time{
aws.Time(timestamp),
Timestamps: []time.Time{
timestamp,
},
Values: []*float64{aws.Float64(23)},
StatusCode: aws.String("Complete"),
Values: []float64{23},
StatusCode: cloudwatchtypes.StatusCodeComplete,
},
{
Id: aws.String("query2"),
Label: aws.String("Elastic Loading Balancing ApplicationLoadBalancersPerRegion"),
Timestamps: []*time.Time{
aws.Time(timestamp),
Timestamps: []time.Time{
timestamp,
},
Values: []*float64{aws.Float64(23)},
StatusCode: aws.String("Complete"),
Values: []float64{23},
StatusCode: cloudwatchtypes.StatusCodeComplete,
},
},
}
@ -601,15 +603,15 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
t.Run("when `MetricQuery` query has no `GROUP BY` clause", func(t *testing.T) {
timestamp := time.Unix(0, 0)
response := &models.QueryRowResponse{
Metrics: []*cloudwatch.MetricDataResult{
Metrics: []*cloudwatchtypes.MetricDataResult{
{
Id: aws.String("query1"),
Label: aws.String("cloudwatch-default-label"),
Timestamps: []*time.Time{
aws.Time(timestamp),
Timestamps: []time.Time{
timestamp,
},
Values: []*float64{aws.Float64(23)},
StatusCode: aws.String("Complete"),
Values: []float64{23},
StatusCode: cloudwatchtypes.StatusCodeComplete,
},
},
}
@ -635,15 +637,15 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
t.Run("ignore dimensions for raw mode query", func(t *testing.T) {
timestamp := time.Unix(0, 0)
response := &models.QueryRowResponse{
Metrics: []*cloudwatch.MetricDataResult{
Metrics: []*cloudwatchtypes.MetricDataResult{
{
Id: aws.String("lb3"),
Label: aws.String("some label"),
Timestamps: []*time.Time{
aws.Time(timestamp),
Timestamps: []time.Time{
timestamp,
},
Values: []*float64{aws.Float64(23)},
StatusCode: aws.String("Complete"),
Values: []float64{23},
StatusCode: cloudwatchtypes.StatusCodeComplete,
},
},
}
@ -675,21 +677,21 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
t.Run("Parse cloudwatch response", func(t *testing.T) {
timestamp := time.Unix(0, 0)
response := &models.QueryRowResponse{
Metrics: []*cloudwatch.MetricDataResult{
Metrics: []*cloudwatchtypes.MetricDataResult{
{
Id: aws.String("id1"),
Label: aws.String("some label"),
Timestamps: []*time.Time{
aws.Time(timestamp),
aws.Time(timestamp.Add(time.Minute)),
aws.Time(timestamp.Add(3 * time.Minute)),
Timestamps: []time.Time{
timestamp,
timestamp.Add(time.Minute),
timestamp.Add(3 * time.Minute),
},
Values: []*float64{
aws.Float64(10),
aws.Float64(20),
aws.Float64(30),
Values: []float64{
10,
20,
30,
},
StatusCode: aws.String("Complete"),
StatusCode: cloudwatchtypes.StatusCodeComplete,
},
},
}
@ -717,9 +719,9 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
assert.Equal(t, "some label", frame.Name)
assert.Equal(t, "Time", frame.Fields[0].Name)
assert.Equal(t, "lb", frame.Fields[1].Labels["LoadBalancer"])
assert.Equal(t, 10.0, *frame.Fields[1].At(0).(*float64))
assert.Equal(t, 20.0, *frame.Fields[1].At(1).(*float64))
assert.Equal(t, 30.0, *frame.Fields[1].At(2).(*float64))
assert.Equal(t, 10.0, frame.Fields[1].At(0).(float64))
assert.Equal(t, 20.0, frame.Fields[1].At(1).(float64))
assert.Equal(t, 30.0, frame.Fields[1].At(2).(float64))
assert.Equal(t, "Value", frame.Fields[1].Name)
assert.Equal(t, "", frame.Fields[1].Config.DisplayName)
})

View File

@ -1,56 +0,0 @@
package routes
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
)
func AccountsHandler(ctx context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
region := parameters.Get("region")
if region == "" {
return nil, models.NewHttpError("error in AccountsHandler", http.StatusBadRequest, fmt.Errorf("region is required"))
}
service, err := newAccountsService(ctx, pluginCtx, reqCtxFactory, region)
if err != nil {
return nil, models.NewHttpError("error in AccountsHandler", http.StatusInternalServerError, err)
}
accounts, err := service.GetAccountsForCurrentUserOrRole(ctx)
if err != nil {
msg := "error getting accounts for current user or role"
switch {
case errors.Is(err, services.ErrAccessDeniedException):
return nil, models.NewHttpError(msg, http.StatusForbidden, err)
default:
return nil, models.NewHttpError(msg, http.StatusInternalServerError, err)
}
}
accountsResponse, err := json.Marshal(accounts)
if err != nil {
return nil, models.NewHttpError("error in AccountsHandler", http.StatusInternalServerError, err)
}
return accountsResponse, nil
}
// newAccountService is an account service factory.
//
// Stubbable by tests.
var newAccountsService = func(ctx context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.AccountsProvider, error) {
oamClient, err := reqCtxFactory(ctx, pluginCtx, region)
if err != nil {
return nil, err
}
return services.NewAccountsService(oamClient.OAMAPIProvider), nil
}

View File

@ -1,55 +0,0 @@
package routes
import (
"context"
"encoding/json"
"net/http"
"net/url"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
)
func DimensionKeysHandler(ctx context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
dimensionKeysRequest, err := resources.GetDimensionKeysRequest(parameters)
if err != nil {
return nil, models.NewHttpError("error in DimensionKeyHandler", http.StatusBadRequest, err)
}
service, err := newListMetricsService(ctx, pluginCtx, reqCtxFactory, dimensionKeysRequest.Region)
if err != nil {
return nil, models.NewHttpError("error in DimensionKeyHandler", http.StatusInternalServerError, err)
}
var response []resources.ResourceResponse[string]
switch dimensionKeysRequest.Type() {
case resources.FilterDimensionKeysRequest:
response, err = service.GetDimensionKeysByDimensionFilter(ctx, dimensionKeysRequest)
default:
response, err = services.GetHardCodedDimensionKeysByNamespace(dimensionKeysRequest.Namespace)
}
if err != nil {
return nil, models.NewHttpError("error in DimensionKeyHandler", http.StatusInternalServerError, err)
}
jsonResponse, err := json.Marshal(response)
if err != nil {
return nil, models.NewHttpError("error in DimensionKeyHandler", http.StatusInternalServerError, err)
}
return jsonResponse, nil
}
// newListMetricsService is an list metrics service factory.
//
// Stubbable by tests.
var newListMetricsService = func(ctx context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
metricClient, err := reqCtxFactory(ctx, pluginCtx, region)
if err != nil {
return nil, err
}
return services.NewListMetricsService(metricClient.MetricsClientProvider), nil
}

View File

@ -1,36 +0,0 @@
package routes
import (
"context"
"encoding/json"
"net/http"
"net/url"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
)
func DimensionValuesHandler(ctx context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
dimensionValuesRequest, err := resources.GetDimensionValuesRequest(parameters)
if err != nil {
return nil, models.NewHttpError("error in DimensionValuesHandler", http.StatusBadRequest, err)
}
service, err := newListMetricsService(ctx, pluginCtx, reqCtxFactory, dimensionValuesRequest.Region)
if err != nil {
return nil, models.NewHttpError("error in DimensionValuesHandler", http.StatusInternalServerError, err)
}
response, err := service.GetDimensionValuesByDimensionFilter(ctx, dimensionValuesRequest)
if err != nil {
return nil, models.NewHttpError("error in DimensionValuesHandler", http.StatusInternalServerError, err)
}
dimensionValuesResponse, err := json.Marshal(response)
if err != nil {
return nil, models.NewHttpError("error in DimensionValuesHandler", http.StatusInternalServerError, err)
}
return dimensionValuesResponse, nil
}

View File

@ -1,32 +0,0 @@
package routes
import (
"context"
"encoding/json"
"net/http"
"net/url"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
)
type ExternalIdResponse struct {
ExternalId string `json:"externalId"`
}
func ExternalIdHandler(ctx context.Context, pluginCtx backend.PluginContext, reqCtxBeforeAuth models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
reqCtx, err := reqCtxBeforeAuth(ctx, pluginCtx, "")
if err != nil {
return nil, models.NewHttpError("error in ExternalIdHandler", http.StatusInternalServerError, err)
}
response := ExternalIdResponse{
ExternalId: reqCtx.Settings.GrafanaSettings.ExternalID,
}
jsonResponse, err := json.Marshal(response)
if err != nil {
return nil, models.NewHttpError("error in ExternalIdHandler", http.StatusInternalServerError, err)
}
return jsonResponse, nil
}

View File

@ -1,52 +0,0 @@
package routes
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/stretchr/testify/assert"
)
func Test_external_id_route(t *testing.T) {
t.Run("successfully returns an external id from the instance", func(t *testing.T) {
t.Setenv("AWS_AUTH_EXTERNAL_ID", "mock-external-id")
rr := httptest.NewRecorder()
factoryFunc := func(_ context.Context, _ backend.PluginContext, region string) (reqCtx models.RequestContext, err error) {
return models.RequestContext{
Settings: models.CloudWatchSettings{
GrafanaSettings: awsds.AuthSettings{ExternalID: "mock-external-id"},
},
}, nil
}
handler := http.HandlerFunc(ResourceRequestMiddleware(ExternalIdHandler, logger, factoryFunc))
req := httptest.NewRequest("GET", "/external-id", nil)
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.JSONEq(t, `{"externalId":"mock-external-id"}`, rr.Body.String())
})
t.Run("returns an empty string if there is no external id", func(t *testing.T) {
rr := httptest.NewRecorder()
factoryFunc := func(_ context.Context, _ backend.PluginContext, region string) (reqCtx models.RequestContext, err error) {
return models.RequestContext{
Settings: models.CloudWatchSettings{},
}, nil
}
handler := http.HandlerFunc(ResourceRequestMiddleware(ExternalIdHandler, logger, factoryFunc))
req := httptest.NewRequest("GET", "/external-id", nil)
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.JSONEq(t, `{"externalId":""}`, rr.Body.String())
})
}

View File

@ -1,22 +0,0 @@
package routes
import (
"encoding/json"
"net/http"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
)
func respondWithError(rw http.ResponseWriter, httpError *models.HttpError) {
response, err := json.Marshal(httpError)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(httpError.StatusCode)
_, err = rw.Write(response)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
}
}

View File

@ -1,36 +0,0 @@
package routes
import (
"context"
"encoding/json"
"net/http"
"net/url"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
)
func LogGroupFieldsHandler(ctx context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
request, err := resources.ParseLogGroupFieldsRequest(parameters)
if err != nil {
return nil, models.NewHttpError("error in LogGroupFieldsHandler", http.StatusBadRequest, err)
}
service, err := newLogGroupsService(ctx, pluginCtx, reqCtxFactory, request.Region)
if err != nil {
return nil, models.NewHttpError("newLogGroupsService error", http.StatusInternalServerError, err)
}
logGroupFields, err := service.GetLogGroupFieldsWithContext(ctx, request)
if err != nil {
return nil, models.NewHttpError("GetLogGroupFields error", http.StatusInternalServerError, err)
}
logGroupsResponse, err := json.Marshal(logGroupFields)
if err != nil {
return nil, models.NewHttpError("LogGroupFieldsHandler json error", http.StatusInternalServerError, err)
}
return logGroupsResponse, nil
}

View File

@ -1,50 +0,0 @@
package routes
import (
"context"
"encoding/json"
"net/http"
"net/url"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
)
func LogGroupsHandler(ctx context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
request, err := resources.ParseLogGroupsRequest(parameters)
if err != nil {
return nil, models.NewHttpError("cannot set both log group name prefix and pattern", http.StatusBadRequest, err)
}
service, err := newLogGroupsService(ctx, pluginCtx, reqCtxFactory, request.Region)
if err != nil {
return nil, models.NewHttpError("newLogGroupsService error", http.StatusInternalServerError, err)
}
logGroups, err := service.GetLogGroupsWithContext(ctx, request)
if err != nil {
return nil, models.NewHttpError("GetLogGroups error", http.StatusInternalServerError, err)
}
logGroupsResponse, err := json.Marshal(logGroups)
if err != nil {
return nil, models.NewHttpError("LogGroupsHandler json error", http.StatusInternalServerError, err)
}
return logGroupsResponse, nil
}
// newLogGroupsService is a describe log groups service factory.
//
// Stubbable by tests.
var newLogGroupsService = func(ctx context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) {
reqCtx, err := reqCtxFactory(ctx, pluginCtx, region)
if err != nil {
return nil, err
}
return services.NewLogGroupsService(reqCtx.LogsAPIProvider, features.IsEnabled(ctx, features.FlagCloudWatchCrossAccountQuerying)), nil
}

View File

@ -1,239 +0,0 @@
package routes
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
)
func TestLogGroupsRoute(t *testing.T) {
origLogGroupsService := newLogGroupsService
t.Cleanup(func() {
newLogGroupsService = origLogGroupsService
})
reqCtxFunc := func(_ context.Context, pluginCtx backend.PluginContext, region string) (reqCtx models.RequestContext, err error) {
return models.RequestContext{}, err
}
t.Run("successfully returns 1 log group with account id", func(t *testing.T) {
mockLogsService := mocks.LogsService{}
mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{{
Value: resources.LogGroup{
Arn: "some arn",
Name: "some name",
},
AccountId: utils.Pointer("111"),
}}, nil)
newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) {
return &mockLogsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupsHandler, logger, reqCtxFunc))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.JSONEq(t, `[{"value":{"name":"some name", "arn":"some arn"},"accountId":"111"}]`, rr.Body.String())
})
t.Run("successfully returns multiple log groups with account id", func(t *testing.T) {
mockLogsService := mocks.LogsService{}
mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return(
[]resources.ResourceResponse[resources.LogGroup]{
{
Value: resources.LogGroup{
Arn: "arn 1",
Name: "name 1",
},
AccountId: utils.Pointer("111"),
}, {
Value: resources.LogGroup{
Arn: "arn 2",
Name: "name 2",
},
AccountId: utils.Pointer("222"),
},
}, nil)
newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) {
return &mockLogsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupsHandler, logger, reqCtxFunc))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.JSONEq(t, `[
{
"value":{
"name":"name 1",
"arn":"arn 1"
},
"accountId":"111"
},
{
"value":{
"name":"name 2",
"arn":"arn 2"
},
"accountId":"222"
}
]`, rr.Body.String())
})
t.Run("returns error when both logGroupPrefix and logGroup Pattern are provided", func(t *testing.T) {
mockLogsService := mocks.LogsService{}
mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) {
return &mockLogsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups?logGroupNamePrefix=some-prefix&logGroupPattern=some-pattern", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupsHandler, logger, reqCtxFunc))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.JSONEq(t, `{"Error":"cannot set both log group name prefix and pattern", "Message":"cannot set both log group name prefix and pattern: cannot set both log group name prefix and pattern", "StatusCode":400}`, rr.Body.String())
})
t.Run("passes default log group limit and nil for logGroupNamePrefix, accountId, and logGroupPattern", func(t *testing.T) {
mockLogsService := mocks.LogsService{}
mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) {
return &mockLogsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupsHandler, logger, reqCtxFunc))
handler.ServeHTTP(rr, req)
mockLogsService.AssertCalled(t, "GetLogGroupsWithContext", resources.LogGroupsRequest{
Limit: 50,
ResourceRequest: resources.ResourceRequest{},
LogGroupNamePrefix: nil,
LogGroupNamePattern: nil,
})
})
t.Run("passes default log group limit and nil for logGroupNamePrefix when both are absent", func(t *testing.T) {
mockLogsService := mocks.LogsService{}
mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) {
return &mockLogsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupsHandler, logger, reqCtxFunc))
handler.ServeHTTP(rr, req)
mockLogsService.AssertCalled(t, "GetLogGroupsWithContext", resources.LogGroupsRequest{
Limit: 50,
LogGroupNamePrefix: nil,
})
})
t.Run("passes log group limit from query parameter", func(t *testing.T) {
mockLogsService := mocks.LogsService{}
mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) {
return &mockLogsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups?limit=2", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupsHandler, logger, reqCtxFunc))
handler.ServeHTTP(rr, req)
mockLogsService.AssertCalled(t, "GetLogGroupsWithContext", resources.LogGroupsRequest{
Limit: 2,
})
})
t.Run("passes logGroupPrefix from query parameter", func(t *testing.T) {
mockLogsService := mocks.LogsService{}
mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) {
return &mockLogsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups?logGroupNamePrefix=some-prefix", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupsHandler, logger, reqCtxFunc))
handler.ServeHTTP(rr, req)
mockLogsService.AssertCalled(t, "GetLogGroupsWithContext", resources.LogGroupsRequest{
Limit: 50,
LogGroupNamePrefix: utils.Pointer("some-prefix"),
})
})
t.Run("passes logGroupPattern from query parameter", func(t *testing.T) {
mockLogsService := mocks.LogsService{}
mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) {
return &mockLogsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups?logGroupPattern=some-pattern", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupsHandler, logger, reqCtxFunc))
handler.ServeHTTP(rr, req)
mockLogsService.AssertCalled(t, "GetLogGroupsWithContext", resources.LogGroupsRequest{
Limit: 50,
LogGroupNamePattern: utils.Pointer("some-pattern"),
})
})
t.Run("passes logGroupPattern from query parameter", func(t *testing.T) {
mockLogsService := mocks.LogsService{}
mockLogsService.On("GetLogGroupsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) {
return &mockLogsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups?accountId=some-account-id", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupsHandler, logger, reqCtxFunc))
handler.ServeHTTP(rr, req)
mockLogsService.AssertCalled(t, "GetLogGroupsWithContext", resources.LogGroupsRequest{
Limit: 50,
ResourceRequest: resources.ResourceRequest{AccountId: utils.Pointer("some-account-id")},
})
})
t.Run("returns error if service returns error", func(t *testing.T) {
mockLogsService := mocks.LogsService{}
mockLogsService.On("GetLogGroupsWithContext", mock.Anything).
Return([]resources.ResourceResponse[resources.LogGroup]{}, fmt.Errorf("some error"))
newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) {
return &mockLogsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/log-groups", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupsHandler, logger, reqCtxFunc))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
assert.JSONEq(t, `{"Error":"some error","Message":"GetLogGroups error: some error","StatusCode":500}`, rr.Body.String())
})
}

View File

@ -1,45 +0,0 @@
package routes
import (
"context"
"encoding/json"
"net/http"
"net/url"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
)
func MetricsHandler(ctx context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
metricsRequest, err := resources.GetMetricsRequest(parameters)
if err != nil {
return nil, models.NewHttpError("error in MetricsHandler", http.StatusBadRequest, err)
}
service, err := newListMetricsService(ctx, pluginCtx, reqCtxFactory, metricsRequest.Region)
if err != nil {
return nil, models.NewHttpError("error in MetricsHandler", http.StatusInternalServerError, err)
}
var response []resources.ResourceResponse[resources.Metric]
switch metricsRequest.Type() {
case resources.AllMetricsRequestType:
response = services.GetAllHardCodedMetrics()
case resources.MetricsByNamespaceRequestType:
response, err = services.GetHardCodedMetricsByNamespace(metricsRequest.Namespace)
case resources.CustomNamespaceRequestType:
response, err = service.GetMetricsByNamespace(ctx, metricsRequest)
}
if err != nil {
return nil, models.NewHttpError("error in MetricsHandler", http.StatusInternalServerError, err)
}
metricsResponse, err := json.Marshal(response)
if err != nil {
return nil, models.NewHttpError("error in MetricsHandler", http.StatusInternalServerError, err)
}
return metricsResponse, nil
}

View File

@ -1,35 +0,0 @@
package routes
import (
"net/http"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
)
func ResourceRequestMiddleware(handleFunc models.RouteHandlerFunc, logger log.Logger, reqCtxFactory models.RequestContextFactoryFunc) func(rw http.ResponseWriter, req *http.Request) {
return func(rw http.ResponseWriter, req *http.Request) {
if req.Method != "GET" {
respondWithError(rw, models.NewHttpError("Invalid method", http.StatusMethodNotAllowed, nil))
return
}
ctx := req.Context()
pluginContext := backend.PluginConfigFromContext(ctx)
json, httpError := handleFunc(ctx, pluginContext, reqCtxFactory, req.URL.Query())
if httpError != nil {
logger.FromContext(ctx).Error("Error handling resource request", "error", httpError.Message)
respondWithError(rw, httpError)
return
}
rw.Header().Set("Content-Type", "application/json")
_, err := rw.Write(json)
if err != nil {
logger.FromContext(ctx).Error("Error handling resource request", "error", err)
respondWithError(rw, models.NewHttpError("error writing response in resource request middleware", http.StatusInternalServerError, err))
}
}
}

View File

@ -1,50 +0,0 @@
package routes
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
)
func Test_Middleware(t *testing.T) {
t.Run("rejects POST method", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/dimension-keys?region=us-east-1", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
return []byte{}, nil
}, logger, nil))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusMethodNotAllowed, rr.Code)
})
t.Run("injects plugincontext to handler", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/some-path", nil)
var testPluginContext backend.PluginContext
handler := http.HandlerFunc(ResourceRequestMiddleware(func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
testPluginContext = pluginCtx
return []byte{}, nil
}, logger, nil))
handler.ServeHTTP(rr, req)
assert.NotNil(t, testPluginContext)
})
t.Run("should propagate handler error to response", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/some-path", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
return []byte{}, models.NewHttpError("error", http.StatusBadRequest, fmt.Errorf("error from handler"))
}, logger, nil))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Equal(t, `{"Message":"error: error from handler","Error":"error from handler","StatusCode":400}`, rr.Body.String())
})
}

View File

@ -1,41 +0,0 @@
package routes
import (
"context"
"encoding/json"
"net/http"
"net/url"
"sort"
"strings"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
)
func NamespacesHandler(ctx context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, _ url.Values) ([]byte, *models.HttpError) {
reqCtx, err := reqCtxFactory(ctx, pluginCtx, "default")
if err != nil {
return nil, models.NewHttpError("error in NamespacesHandler", http.StatusInternalServerError, err)
}
response := services.GetHardCodedNamespaces()
customNamespace := reqCtx.Settings.Namespace
if customNamespace != "" {
customNamespaces := strings.Split(customNamespace, ",")
for _, customNamespace := range customNamespaces {
response = append(response, resources.ResourceResponse[string]{Value: customNamespace})
}
}
sort.Slice(response, func(i, j int) bool {
return response[i].Value < response[j].Value
})
namespacesResponse, err := json.Marshal(response)
if err != nil {
return nil, models.NewHttpError("error in NamespacesHandler", http.StatusInternalServerError, err)
}
return namespacesResponse, nil
}

View File

@ -1,48 +0,0 @@
package routes
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/url"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
)
const (
defaultRegion = "default"
)
func RegionsHandler(ctx context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
service, err := newRegionsService(ctx, pluginCtx, reqCtxFactory, defaultRegion)
if err != nil {
if errors.Is(err, models.ErrMissingRegion) {
return nil, models.NewHttpError("Error in Regions Handler when connecting to aws without a default region selection", http.StatusBadRequest, err)
}
return nil, models.NewHttpError("Error in Regions Handler when connecting to aws", http.StatusInternalServerError, err)
}
regions, err := service.GetRegions(ctx)
if err != nil {
return nil, models.NewHttpError("Error in Regions Handler while fetching regions", http.StatusInternalServerError, err)
}
regionsResponse, err := json.Marshal(regions)
if err != nil {
return nil, models.NewHttpError("Error in Regions Handler while parsing regions", http.StatusInternalServerError, err)
}
return regionsResponse, nil
}
var newRegionsService = func(ctx context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.RegionsAPIProvider, error) {
reqCtx, err := reqCtxFactory(ctx, pluginCtx, region)
if err != nil {
return nil, err
}
return services.NewRegionsService(reqCtx.EC2APIProvider, reqCtx.Logger), nil
}

View File

@ -1,84 +0,0 @@
package routes
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestRegionsRoute(t *testing.T) {
origNewRegionsService := newRegionsService
t.Cleanup(func() {
newRegionsService = origNewRegionsService
})
t.Run("returns 200 and regions", func(t *testing.T) {
mockRegionService := mocks.RegionsService{}
newRegionsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.RegionsAPIProvider, error) {
return &mockRegionService, nil
}
mockRegionService.On("GetRegions", mock.Anything).Return([]resources.ResourceResponse[resources.Region]{{
Value: resources.Region{
Name: "us-east-1",
},
}}, nil).Once()
rr := httptest.NewRecorder()
handler := http.HandlerFunc(ResourceRequestMiddleware(RegionsHandler, logger, nil))
req := httptest.NewRequest("GET", `/regions`, nil)
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "us-east-1")
})
t.Run("returns 400 when the service returns a missing region error", func(t *testing.T) {
newRegionsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.RegionsAPIProvider, error) {
return nil, models.ErrMissingRegion
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(ResourceRequestMiddleware(RegionsHandler, logger, nil))
req := httptest.NewRequest("GET", `/regions`, nil)
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Error in Regions Handler when connecting to aws without a default region selection: missing default region")
})
t.Run("returns 500 when the service returns an unexpected error", func(t *testing.T) {
newRegionsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.RegionsAPIProvider, error) {
return nil, errors.New("something unexpected happened")
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(ResourceRequestMiddleware(RegionsHandler, logger, nil))
req := httptest.NewRequest("GET", `/regions`, nil)
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
assert.Contains(t, rr.Body.String(), "Error in Regions Handler when connecting to aws: something unexpected happened")
})
t.Run("returns 500 when get regions returns an error", func(t *testing.T) {
mockRegionService := mocks.RegionsService{}
newRegionsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.RegionsAPIProvider, error) {
return &mockRegionService, nil
}
mockRegionService.On("GetRegions", mock.Anything).Return([]resources.ResourceResponse[resources.Region](nil), errors.New("aws is having some kind of outage")).Once()
rr := httptest.NewRecorder()
handler := http.HandlerFunc(ResourceRequestMiddleware(RegionsHandler, logger, nil))
req := httptest.NewRequest("GET", `/regions`, nil)
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
assert.Contains(t, rr.Body.String(), "Error in Regions Handler while fetching regions: aws is having some kind of outage")
})
}

View File

@ -0,0 +1,43 @@
package cloudwatch
import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
)
func ProvideService() *Service {
return &Service{
datasource.NewInstanceManager(NewDatasource),
}
}
type Service struct {
im instancemgmt.InstanceManager
}
func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
instance, err := s.im.Get(ctx, req.PluginContext)
if err != nil {
return nil, err
}
return instance.(*DataSource).CheckHealth(ctx, req)
}
func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
instance, err := s.im.Get(ctx, req.PluginContext)
if err != nil {
return err
}
return instance.(*DataSource).CallResource(ctx, req, sender)
}
func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
instance, err := s.im.Get(ctx, req.PluginContext)
if err != nil {
return nil, err
}
return instance.(*DataSource).QueryData(ctx, req)
}

View File

@ -4,9 +4,10 @@ import (
"context"
"errors"
"fmt"
"strings"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/oam"
oam "github.com/aws/aws-sdk-go-v2/service/oam"
oamtypes "github.com/aws/aws-sdk-go-v2/service/oam/types"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
)
@ -17,26 +18,20 @@ type AccountsService struct {
models.OAMAPIProvider
}
func NewAccountsService(oamClient models.OAMAPIProvider) models.AccountsProvider {
var NewAccountsService = func(oamClient models.OAMAPIProvider) models.AccountsProvider {
return &AccountsService{oamClient}
}
func (a *AccountsService) GetAccountsForCurrentUserOrRole(ctx context.Context) ([]resources.ResourceResponse[resources.Account], error) {
var nextToken *string
sinks := []*oam.ListSinksItem{}
sinks := []oamtypes.ListSinksItem{}
for {
response, err := a.ListSinksWithContext(ctx, &oam.ListSinksInput{NextToken: nextToken})
response, err := a.ListSinks(ctx, &oam.ListSinksInput{NextToken: nextToken})
if err != nil {
var aerr awserr.Error
if errors.As(err, &aerr) {
switch aerr.Code() {
// unlike many other services, OAM doesn't define this error code. however, it's returned in case calling role/user has insufficient permissions
case "AccessDeniedException":
return nil, fmt.Errorf("%w: %s", ErrAccessDeniedException, aerr.Message())
}
// TODO: this is a bit hacky, figure out how to do it right in v2
if strings.Contains(err.Error(), "AccessDeniedException") {
return nil, fmt.Errorf("%w: %s", ErrAccessDeniedException, err.Error())
}
}
if err != nil {
return nil, fmt.Errorf("ListSinks error: %w", err)
}
@ -62,7 +57,7 @@ func (a *AccountsService) GetAccountsForCurrentUserOrRole(ctx context.Context) (
nextToken = nil
for {
links, err := a.ListAttachedLinksWithContext(ctx, &oam.ListAttachedLinksInput{
links, err := a.ListAttachedLinks(ctx, &oam.ListAttachedLinksInput{
SinkIdentifier: sinkIdentifier,
NextToken: nextToken,
})
@ -86,5 +81,5 @@ func (a *AccountsService) GetAccountsForCurrentUserOrRole(ctx context.Context) (
nextToken = links.NextToken
}
return valuesToListMetricRespone(response), nil
return valuesToListMetricResponse(response), nil
}

View File

@ -2,12 +2,14 @@ package services
import (
"context"
"errors"
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/oam"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/oam"
oamtypes "github.com/aws/aws-sdk-go-v2/service/oam/types"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/stretchr/testify/assert"
@ -18,21 +20,20 @@ import (
func TestHandleGetAccounts(t *testing.T) {
t.Run("Should return an error in case of insufficient permissions from ListSinks", func(t *testing.T) {
fakeOAMClient := &mocks.FakeOAMClient{}
fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{}, awserr.New("AccessDeniedException",
"AWS message", nil))
fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{}, errors.New("AccessDeniedException"))
accounts := NewAccountsService(fakeOAMClient)
resp, err := accounts.GetAccountsForCurrentUserOrRole(context.Background())
assert.Error(t, err)
assert.Nil(t, resp)
assert.Equal(t, err.Error(), "access denied. please check your IAM policy: AWS message")
assert.Equal(t, "access denied. please check your IAM policy: AccessDeniedException", err.Error())
assert.ErrorIs(t, err, ErrAccessDeniedException)
})
t.Run("Should return an error in case of any error from ListSinks", func(t *testing.T) {
fakeOAMClient := &mocks.FakeOAMClient{}
fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{}, fmt.Errorf("some error"))
fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{}, fmt.Errorf("some error"))
accounts := NewAccountsService(fakeOAMClient)
resp, err := accounts.GetAccountsForCurrentUserOrRole(context.Background())
@ -44,7 +45,7 @@ func TestHandleGetAccounts(t *testing.T) {
t.Run("Should return empty array in case no monitoring account exists", func(t *testing.T) {
fakeOAMClient := &mocks.FakeOAMClient{}
fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{}, nil)
fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{}, nil)
accounts := NewAccountsService(fakeOAMClient)
resp, err := accounts.GetAccountsForCurrentUserOrRole(context.Background())
@ -55,26 +56,26 @@ func TestHandleGetAccounts(t *testing.T) {
t.Run("Should return one monitoring account (the first) even though ListSinks returns multiple sinks", func(t *testing.T) {
fakeOAMClient := &mocks.FakeOAMClient{}
fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{
Items: []*oam.ListSinksItem{
fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{
Items: []oamtypes.ListSinksItem{
{Name: aws.String("Account 1"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group1")},
{Name: aws.String("Account 2"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group2")},
},
NextToken: new(string),
}, nil).Once()
fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{
Items: []*oam.ListSinksItem{
fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{
Items: []oamtypes.ListSinksItem{
{Name: aws.String("Account 3"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group3")},
},
NextToken: nil,
}, nil)
fakeOAMClient.On("ListAttachedLinksWithContext", mock.Anything).Return(&oam.ListAttachedLinksOutput{}, nil)
fakeOAMClient.On("ListAttachedLinks", mock.Anything).Return(&oam.ListAttachedLinksOutput{}, nil)
accounts := NewAccountsService(fakeOAMClient)
resp, err := accounts.GetAccountsForCurrentUserOrRole(context.Background())
assert.NoError(t, err)
fakeOAMClient.AssertNumberOfCalls(t, "ListSinksWithContext", 2)
fakeOAMClient.AssertNumberOfCalls(t, "ListSinks", 2)
require.Len(t, resp, 1)
assert.True(t, resp[0].Value.IsMonitoringAccount)
assert.Equal(t, "Account 1", resp[0].Value.Label)
@ -83,28 +84,28 @@ func TestHandleGetAccounts(t *testing.T) {
t.Run("Should merge the first sink with attached links", func(t *testing.T) {
fakeOAMClient := &mocks.FakeOAMClient{}
fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{
Items: []*oam.ListSinksItem{
fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{
Items: []oamtypes.ListSinksItem{
{Name: aws.String("Account 1"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group1")},
{Name: aws.String("Account 2"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group2")},
},
NextToken: new(string),
}, nil).Once()
fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{
Items: []*oam.ListSinksItem{
fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{
Items: []oamtypes.ListSinksItem{
{Name: aws.String("Account 3"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group3")},
},
NextToken: nil,
}, nil)
fakeOAMClient.On("ListAttachedLinksWithContext", mock.Anything).Return(&oam.ListAttachedLinksOutput{
Items: []*oam.ListAttachedLinksItem{
fakeOAMClient.On("ListAttachedLinks", mock.Anything).Return(&oam.ListAttachedLinksOutput{
Items: []oamtypes.ListAttachedLinksItem{
{Label: aws.String("Account 10"), LinkArn: aws.String("arn:aws:logs:us-east-1:123456789013:log-group:my-log-group10")},
{Label: aws.String("Account 11"), LinkArn: aws.String("arn:aws:logs:us-east-1:123456789014:log-group:my-log-group11")},
},
NextToken: new(string),
}, nil).Once()
fakeOAMClient.On("ListAttachedLinksWithContext", mock.Anything).Return(&oam.ListAttachedLinksOutput{
Items: []*oam.ListAttachedLinksItem{
fakeOAMClient.On("ListAttachedLinks", mock.Anything).Return(&oam.ListAttachedLinksOutput{
Items: []oamtypes.ListAttachedLinksItem{
{Label: aws.String("Account 12"), LinkArn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group12")},
},
NextToken: nil,
@ -114,8 +115,8 @@ func TestHandleGetAccounts(t *testing.T) {
resp, err := accounts.GetAccountsForCurrentUserOrRole(context.Background())
assert.NoError(t, err)
fakeOAMClient.AssertNumberOfCalls(t, "ListSinksWithContext", 2)
fakeOAMClient.AssertNumberOfCalls(t, "ListAttachedLinksWithContext", 2)
fakeOAMClient.AssertNumberOfCalls(t, "ListSinks", 2)
fakeOAMClient.AssertNumberOfCalls(t, "ListAttachedLinks", 2)
expectedAccounts := []resources.ResourceResponse[resources.Account]{
{Value: resources.Account{Id: "123456789012", Label: "Account 1", Arn: "arn:aws:logs:us-east-1:123456789012:log-group:my-log-group1", IsMonitoringAccount: true}},
{Value: resources.Account{Id: "123456789013", Label: "Account 10", Arn: "arn:aws:logs:us-east-1:123456789013:log-group:my-log-group10", IsMonitoringAccount: false}},
@ -127,34 +128,34 @@ func TestHandleGetAccounts(t *testing.T) {
t.Run("Should call ListAttachedLinks with arn of first sink", func(t *testing.T) {
fakeOAMClient := &mocks.FakeOAMClient{}
fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{
Items: []*oam.ListSinksItem{
fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{
Items: []oamtypes.ListSinksItem{
{Name: aws.String("Account 1"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group1")},
},
NextToken: new(string),
}, nil).Once()
fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{
Items: []*oam.ListSinksItem{
fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{
Items: []oamtypes.ListSinksItem{
{Name: aws.String("Account 3"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group3")},
},
NextToken: nil,
}, nil).Once()
fakeOAMClient.On("ListAttachedLinksWithContext", mock.Anything).Return(&oam.ListAttachedLinksOutput{}, nil)
fakeOAMClient.On("ListAttachedLinks", mock.Anything).Return(&oam.ListAttachedLinksOutput{}, nil)
accounts := NewAccountsService(fakeOAMClient)
_, _ = accounts.GetAccountsForCurrentUserOrRole(context.Background())
fakeOAMClient.AssertCalled(t, "ListAttachedLinksWithContext", &oam.ListAttachedLinksInput{
fakeOAMClient.AssertCalled(t, "ListAttachedLinks", &oam.ListAttachedLinksInput{
SinkIdentifier: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group1"),
})
})
t.Run("Should return an error in case of any error from ListAttachedLinks", func(t *testing.T) {
fakeOAMClient := &mocks.FakeOAMClient{}
fakeOAMClient.On("ListSinksWithContext", mock.Anything).Return(&oam.ListSinksOutput{
Items: []*oam.ListSinksItem{{Name: aws.String("Account 1"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group1")}},
fakeOAMClient.On("ListSinks", mock.Anything).Return(&oam.ListSinksOutput{
Items: []oamtypes.ListSinksItem{{Name: aws.String("Account 1"), Arn: aws.String("arn:aws:logs:us-east-1:123456789012:log-group:my-log-group1")}},
}, nil)
fakeOAMClient.On("ListAttachedLinksWithContext", mock.Anything).Return(&oam.ListAttachedLinksOutput{}, fmt.Errorf("some error")).Once()
fakeOAMClient.On("ListAttachedLinks", mock.Anything).Return(&oam.ListAttachedLinksOutput{}, fmt.Errorf("some error")).Once()
accounts := NewAccountsService(fakeOAMClient)
resp, err := accounts.GetAccountsForCurrentUserOrRole(context.Background())

View File

@ -13,7 +13,7 @@ var GetHardCodedDimensionKeysByNamespace = func(namespace string) ([]resources.R
if response, exists = cloudWatchConsts.NamespaceDimensionKeysMap[namespace]; !exists {
return nil, fmt.Errorf("unable to find dimensions for namespace '%q'", namespace)
}
return valuesToListMetricRespone(response), nil
return valuesToListMetricResponse(response), nil
}
var GetHardCodedMetricsByNamespace = func(namespace string) ([]resources.ResourceResponse[resources.Metric], error) {
@ -28,7 +28,7 @@ var GetHardCodedMetricsByNamespace = func(namespace string) ([]resources.Resourc
response = append(response, resources.Metric{Namespace: namespace, Name: metric})
}
return valuesToListMetricRespone(response), nil
return valuesToListMetricResponse(response), nil
}
var GetAllHardCodedMetrics = func() []resources.ResourceResponse[resources.Metric] {
@ -39,7 +39,7 @@ var GetAllHardCodedMetrics = func() []resources.ResourceResponse[resources.Metri
}
}
return valuesToListMetricRespone(response)
return valuesToListMetricResponse(response)
}
var GetHardCodedNamespaces = func() []resources.ResourceResponse[string] {
@ -48,5 +48,5 @@ var GetHardCodedNamespaces = func() []resources.ResourceResponse[string] {
response = append(response, key)
}
return valuesToListMetricRespone(response)
return valuesToListMetricResponse(response)
}

View File

@ -5,8 +5,10 @@ import (
"fmt"
"sort"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
)
@ -15,7 +17,7 @@ type ListMetricsService struct {
models.MetricsClientProvider
}
func NewListMetricsService(metricsClient models.MetricsClientProvider) models.ListMetricsProvider {
var NewListMetricsService = func(metricsClient models.MetricsClientProvider) models.ListMetricsProvider {
return &ListMetricsService{metricsClient}
}
@ -30,7 +32,7 @@ func (l *ListMetricsService) GetDimensionKeysByDimensionFilter(ctx context.Conte
setDimensionFilter(input, r.DimensionFilter)
setAccount(input, r.ResourceRequest)
metrics, err := l.ListMetricsWithPageLimit(ctx, input)
accountMetrics, err := l.ListMetricsWithPageLimit(ctx, input)
if err != nil {
return nil, fmt.Errorf("%v: %w", "unable to call AWS API", err)
}
@ -38,8 +40,8 @@ func (l *ListMetricsService) GetDimensionKeysByDimensionFilter(ctx context.Conte
response := []resources.ResourceResponse[string]{}
// remove duplicates
dupCheck := make(map[string]struct{})
for _, metric := range metrics {
for _, dim := range metric.Dimensions {
for _, accountMetric := range accountMetrics {
for _, dim := range accountMetric.Metric.Dimensions {
if _, exists := dupCheck[*dim.Name]; exists {
continue
}
@ -58,7 +60,7 @@ func (l *ListMetricsService) GetDimensionKeysByDimensionFilter(ctx context.Conte
}
dupCheck[*dim.Name] = struct{}{}
response = append(response, resources.ResourceResponse[string]{AccountId: metric.AccountId, Value: *dim.Name})
response = append(response, resources.ResourceResponse[string]{AccountId: accountMetric.AccountId, Value: *dim.Name})
}
}
@ -73,15 +75,15 @@ func (l *ListMetricsService) GetDimensionValuesByDimensionFilter(ctx context.Con
setDimensionFilter(input, r.DimensionFilter)
setAccount(input, r.ResourceRequest)
metrics, err := l.ListMetricsWithPageLimit(ctx, input)
accountMetrics, err := l.ListMetricsWithPageLimit(ctx, input)
if err != nil {
return nil, fmt.Errorf("%v: %w", "unable to call AWS API", err)
}
response := []resources.ResourceResponse[string]{}
dupCheck := make(map[string]bool)
for _, metric := range metrics {
for _, dim := range metric.Dimensions {
for _, metric := range accountMetrics {
for _, dim := range metric.Metric.Dimensions {
if *dim.Name == r.DimensionKey {
if _, exists := dupCheck[*dim.Value]; exists {
continue
@ -102,19 +104,19 @@ func (l *ListMetricsService) GetDimensionValuesByDimensionFilter(ctx context.Con
func (l *ListMetricsService) GetMetricsByNamespace(ctx context.Context, r resources.MetricsRequest) ([]resources.ResourceResponse[resources.Metric], error) {
input := &cloudwatch.ListMetricsInput{Namespace: aws.String(r.Namespace)}
setAccount(input, r.ResourceRequest)
metrics, err := l.ListMetricsWithPageLimit(ctx, input)
accountMetrics, err := l.ListMetricsWithPageLimit(ctx, input)
if err != nil {
return nil, err
}
response := []resources.ResourceResponse[resources.Metric]{}
dupCheck := make(map[string]struct{})
for _, metric := range metrics {
if _, exists := dupCheck[*metric.MetricName]; exists {
for _, accountMetric := range accountMetrics {
if _, exists := dupCheck[*accountMetric.Metric.MetricName]; exists {
continue
}
dupCheck[*metric.MetricName] = struct{}{}
response = append(response, resources.ResourceResponse[resources.Metric]{AccountId: metric.AccountId, Value: resources.Metric{Name: *metric.MetricName, Namespace: *metric.Namespace}})
dupCheck[*accountMetric.Metric.MetricName] = struct{}{}
response = append(response, resources.ResourceResponse[resources.Metric]{AccountId: accountMetric.AccountId, Value: resources.Metric{Name: *accountMetric.Metric.MetricName, Namespace: *accountMetric.Metric.Namespace}})
}
return response, nil
@ -122,7 +124,7 @@ func (l *ListMetricsService) GetMetricsByNamespace(ctx context.Context, r resour
func setDimensionFilter(input *cloudwatch.ListMetricsInput, dimensionFilter []*resources.Dimension) {
for _, dimension := range dimensionFilter {
df := &cloudwatch.DimensionFilter{
df := cloudwatchtypes.DimensionFilter{
Name: aws.String(dimension.Name),
}
if dimension.Value != "" {

View File

@ -4,8 +4,10 @@ import (
"context"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
@ -18,20 +20,20 @@ const useLinkedAccountsId = "all"
var metricResponse = []resources.MetricResponse{
{
Metric: &cloudwatch.Metric{
Metric: cloudwatchtypes.Metric{
MetricName: aws.String("CPUUtilization"),
Namespace: aws.String("AWS/EC2"),
Dimensions: []*cloudwatch.Dimension{
Dimensions: []cloudwatchtypes.Dimension{
{Name: aws.String("InstanceId"), Value: aws.String("i-1234567890abcdef0")},
{Name: aws.String("InstanceType"), Value: aws.String("t2.micro")},
},
},
},
{
Metric: &cloudwatch.Metric{
Metric: cloudwatchtypes.Metric{
MetricName: aws.String("CPUUtilization"),
Namespace: aws.String("AWS/EC2"),
Dimensions: []*cloudwatch.Dimension{
Dimensions: []cloudwatchtypes.Dimension{
{Name: aws.String("InstanceId"), Value: aws.String("i-5234567890abcdef0")},
{Name: aws.String("InstanceType"), Value: aws.String("t2.micro")},
{Name: aws.String("AutoScalingGroupName"), Value: aws.String("my-asg")},
@ -39,10 +41,10 @@ var metricResponse = []resources.MetricResponse{
},
},
{
Metric: &cloudwatch.Metric{
Metric: cloudwatchtypes.Metric{
MetricName: aws.String("CPUUtilization"),
Namespace: aws.String("AWS/EC2"),
Dimensions: []*cloudwatch.Dimension{
Dimensions: []cloudwatchtypes.Dimension{
{Name: aws.String("InstanceId"), Value: aws.String("i-64234567890abcdef0")},
{Name: aws.String("InstanceType"), Value: aws.String("t3.micro")},
{Name: aws.String("AutoScalingGroupName"), Value: aws.String("my-asg2")},
@ -86,7 +88,7 @@ func TestListMetricsService_GetDimensionKeysByDimensionFilter(t *testing.T) {
listMetricsWithPageLimitInput: &cloudwatch.ListMetricsInput{
MetricName: aws.String("CPUUtilization"),
Namespace: aws.String("AWS/EC2"),
Dimensions: []*cloudwatch.DimensionFilter{{Name: aws.String("InstanceId")}},
Dimensions: []cloudwatchtypes.DimensionFilter{{Name: aws.String("InstanceId")}},
IncludeLinkedAccounts: aws.Bool(true),
},
},
@ -101,7 +103,7 @@ func TestListMetricsService_GetDimensionKeysByDimensionFilter(t *testing.T) {
listMetricsWithPageLimitInput: &cloudwatch.ListMetricsInput{
MetricName: aws.String("CPUUtilization"),
Namespace: aws.String("AWS/EC2"),
Dimensions: []*cloudwatch.DimensionFilter{{Name: aws.String("InstanceId")}},
Dimensions: []cloudwatchtypes.DimensionFilter{{Name: aws.String("InstanceId")}},
IncludeLinkedAccounts: aws.Bool(true),
OwningAccount: aws.String("1234567890"),
},
@ -114,7 +116,7 @@ func TestListMetricsService_GetDimensionKeysByDimensionFilter(t *testing.T) {
MetricName: "",
DimensionFilter: []*resources.Dimension{{Name: "InstanceId", Value: ""}},
},
listMetricsWithPageLimitInput: &cloudwatch.ListMetricsInput{Dimensions: []*cloudwatch.DimensionFilter{{Name: aws.String("InstanceId")}}},
listMetricsWithPageLimitInput: &cloudwatch.ListMetricsInput{Dimensions: []cloudwatchtypes.DimensionFilter{{Name: aws.String("InstanceId")}}},
},
}
@ -163,7 +165,7 @@ func TestListMetricsService_GetDimensionValuesByDimensionFilter(t *testing.T) {
listMetricsWithPageLimitInput: &cloudwatch.ListMetricsInput{
MetricName: aws.String("CPUUtilization"),
Namespace: aws.String("AWS/EC2"),
Dimensions: []*cloudwatch.DimensionFilter{{Name: aws.String("InstanceId")}},
Dimensions: []cloudwatchtypes.DimensionFilter{{Name: aws.String("InstanceId")}},
IncludeLinkedAccounts: aws.Bool(true),
},
},
@ -178,7 +180,7 @@ func TestListMetricsService_GetDimensionValuesByDimensionFilter(t *testing.T) {
listMetricsWithPageLimitInput: &cloudwatch.ListMetricsInput{
MetricName: aws.String("CPUUtilization"),
Namespace: aws.String("AWS/EC2"),
Dimensions: []*cloudwatch.DimensionFilter{{Name: aws.String("InstanceId")}},
Dimensions: []cloudwatchtypes.DimensionFilter{{Name: aws.String("InstanceId")}},
IncludeLinkedAccounts: aws.Bool(true),
OwningAccount: aws.String("1234567890"),
},

View File

@ -3,9 +3,9 @@ package services
import (
"context"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
@ -16,13 +16,13 @@ type LogGroupsService struct {
isCrossAccountEnabled bool
}
func NewLogGroupsService(logsClient models.CloudWatchLogsAPIProvider, isCrossAccountEnabled bool) models.LogGroupsProvider {
var NewLogGroupsService = func(logsClient models.CloudWatchLogsAPIProvider, isCrossAccountEnabled bool) models.LogGroupsProvider {
return &LogGroupsService{logGroupsAPI: logsClient, isCrossAccountEnabled: isCrossAccountEnabled}
}
func (s *LogGroupsService) GetLogGroupsWithContext(ctx context.Context, req resources.LogGroupsRequest) ([]resources.ResourceResponse[resources.LogGroup], error) {
func (s *LogGroupsService) GetLogGroups(ctx context.Context, req resources.LogGroupsRequest) ([]resources.ResourceResponse[resources.LogGroup], error) {
input := &cloudwatchlogs.DescribeLogGroupsInput{
Limit: aws.Int64(req.Limit),
Limit: aws.Int32(req.Limit),
LogGroupNamePrefix: req.LogGroupNamePrefix,
}
@ -33,13 +33,13 @@ func (s *LogGroupsService) GetLogGroupsWithContext(ctx context.Context, req reso
}
if !req.IsTargetingAllAccounts() {
// TODO: accept more than one account id in search
input.AccountIdentifiers = []*string{req.AccountId}
input.AccountIdentifiers = []string{*req.AccountId}
}
}
result := []resources.ResourceResponse[resources.LogGroup]{}
for {
response, err := s.logGroupsAPI.DescribeLogGroupsWithContext(ctx, input)
response, err := s.logGroupsAPI.DescribeLogGroups(ctx, input)
if err != nil || response == nil {
return nil, err
}
@ -63,7 +63,7 @@ func (s *LogGroupsService) GetLogGroupsWithContext(ctx context.Context, req reso
return result, nil
}
func (s *LogGroupsService) GetLogGroupFieldsWithContext(ctx context.Context, request resources.LogGroupFieldsRequest, option ...request.Option) ([]resources.ResourceResponse[resources.LogGroupField], error) {
func (s *LogGroupsService) GetLogGroupFields(ctx context.Context, request resources.LogGroupFieldsRequest) ([]resources.ResourceResponse[resources.LogGroupField], error) {
input := &cloudwatchlogs.GetLogGroupFieldsInput{
LogGroupName: aws.String(request.LogGroupName),
}
@ -73,7 +73,7 @@ func (s *LogGroupsService) GetLogGroupFieldsWithContext(ctx context.Context, req
// input.LogGroupName = nil
// }
getLogGroupFieldsOutput, err := s.logGroupsAPI.GetLogGroupFieldsWithContext(ctx, input)
getLogGroupFieldsOutput, err := s.logGroupsAPI.GetLogGroupFields(ctx, input)
if err != nil {
return nil, err
}
@ -83,7 +83,7 @@ func (s *LogGroupsService) GetLogGroupFieldsWithContext(ctx context.Context, req
result = append(result, resources.ResourceResponse[resources.LogGroupField]{
Value: resources.LogGroupField{
Name: *logGroupField.Name,
Percent: *logGroupField.Percent,
Percent: int64(logGroupField.Percent),
},
})
}

View File

@ -5,11 +5,14 @@ import (
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
@ -17,9 +20,9 @@ import (
func TestGetLogGroups(t *testing.T) {
t.Run("Should map log groups response", func(t *testing.T) {
mockLogsAPI := &mocks.LogsAPI{}
mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(
mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(
&cloudwatchlogs.DescribeLogGroupsOutput{
LogGroups: []*cloudwatchlogs.LogGroup{
LogGroups: []cloudwatchlogstypes.LogGroup{
{Arn: utils.Pointer("arn:aws:logs:us-east-1:111:log-group:group_a"), LogGroupName: utils.Pointer("group_a")},
{Arn: utils.Pointer("arn:aws:logs:us-east-1:222:log-group:group_b"), LogGroupName: utils.Pointer("group_b")},
{Arn: utils.Pointer("arn:aws:logs:us-east-1:333:log-group:group_c"), LogGroupName: utils.Pointer("group_c")},
@ -27,7 +30,7 @@ func TestGetLogGroups(t *testing.T) {
}, nil)
service := NewLogGroupsService(mockLogsAPI, false)
resp, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{})
resp, err := service.GetLogGroups(context.Background(), resources.LogGroupsRequest{})
assert.NoError(t, err)
assert.Equal(t, []resources.ResourceResponse[resources.LogGroup]{
@ -48,10 +51,10 @@ func TestGetLogGroups(t *testing.T) {
t.Run("Should return an empty error if api doesn't return any data", func(t *testing.T) {
mockLogsAPI := &mocks.LogsAPI{}
mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
service := NewLogGroupsService(mockLogsAPI, false)
resp, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{})
resp, err := service.GetLogGroups(context.Background(), resources.LogGroupsRequest{})
assert.NoError(t, err)
assert.Equal(t, []resources.ResourceResponse[resources.LogGroup]{}, resp)
@ -60,41 +63,41 @@ func TestGetLogGroups(t *testing.T) {
t.Run("Should only use LogGroupNamePrefix even if LogGroupNamePattern passed in resource call", func(t *testing.T) {
// TODO: use LogGroupNamePattern when we have accounted for its behavior, still a little unexpected at the moment
mockLogsAPI := &mocks.LogsAPI{}
mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
service := NewLogGroupsService(mockLogsAPI, false)
_, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{
_, err := service.GetLogGroups(context.Background(), resources.LogGroupsRequest{
Limit: 0,
LogGroupNamePrefix: utils.Pointer("test"),
})
assert.NoError(t, err)
mockLogsAPI.AssertCalled(t, "DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{
Limit: utils.Pointer(int64(0)),
mockLogsAPI.AssertCalled(t, "DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{
Limit: aws.Int32(0),
LogGroupNamePrefix: utils.Pointer("test"),
})
})
t.Run("Should call api without LogGroupNamePrefix nor LogGroupNamePattern if not passed in resource call", func(t *testing.T) {
mockLogsAPI := &mocks.LogsAPI{}
mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
service := NewLogGroupsService(mockLogsAPI, false)
_, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{})
_, err := service.GetLogGroups(context.Background(), resources.LogGroupsRequest{})
assert.NoError(t, err)
mockLogsAPI.AssertCalled(t, "DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{
Limit: utils.Pointer(int64(0)),
mockLogsAPI.AssertCalled(t, "DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{
Limit: aws.Int32(0),
})
})
t.Run("Should return an error when API returns error", func(t *testing.T) {
mockLogsAPI := &mocks.LogsAPI{}
mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{},
mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{},
fmt.Errorf("some error"))
service := NewLogGroupsService(mockLogsAPI, false)
_, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{})
_, err := service.GetLogGroups(context.Background(), resources.LogGroupsRequest{})
assert.Error(t, err)
assert.Equal(t, "some error", err.Error())
@ -108,21 +111,21 @@ func TestGetLogGroups(t *testing.T) {
ListAllLogGroups: false,
}
mockLogsAPI.On("DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{
Limit: aws.Int64(req.Limit),
mockLogsAPI.On("DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{
Limit: aws.Int32(req.Limit),
LogGroupNamePrefix: req.LogGroupNamePrefix,
}).Return(&cloudwatchlogs.DescribeLogGroupsOutput{
LogGroups: []*cloudwatchlogs.LogGroup{
LogGroups: []cloudwatchlogstypes.LogGroup{
{Arn: utils.Pointer("arn:aws:logs:us-east-1:111:log-group:group_a"), LogGroupName: utils.Pointer("group_a")},
},
NextToken: aws.String("next_token"),
}, nil)
service := NewLogGroupsService(mockLogsAPI, false)
resp, err := service.GetLogGroupsWithContext(context.Background(), req)
resp, err := service.GetLogGroups(context.Background(), req)
assert.NoError(t, err)
mockLogsAPI.AssertNumberOfCalls(t, "DescribeLogGroupsWithContext", 1)
mockLogsAPI.AssertNumberOfCalls(t, "DescribeLogGroups", 1)
assert.Equal(t, []resources.ResourceResponse[resources.LogGroup]{
{
AccountId: utils.Pointer("111"),
@ -140,30 +143,30 @@ func TestGetLogGroups(t *testing.T) {
}
// first call
mockLogsAPI.On("DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{
Limit: aws.Int64(req.Limit),
mockLogsAPI.On("DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{
Limit: aws.Int32(req.Limit),
LogGroupNamePrefix: req.LogGroupNamePrefix,
}).Return(&cloudwatchlogs.DescribeLogGroupsOutput{
LogGroups: []*cloudwatchlogs.LogGroup{
LogGroups: []cloudwatchlogstypes.LogGroup{
{Arn: utils.Pointer("arn:aws:logs:us-east-1:111:log-group:group_a"), LogGroupName: utils.Pointer("group_a")},
},
NextToken: utils.Pointer("token"),
}, nil)
// second call
mockLogsAPI.On("DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{
Limit: aws.Int64(req.Limit),
mockLogsAPI.On("DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{
Limit: aws.Int32(req.Limit),
LogGroupNamePrefix: req.LogGroupNamePrefix,
NextToken: utils.Pointer("token"),
}).Return(&cloudwatchlogs.DescribeLogGroupsOutput{
LogGroups: []*cloudwatchlogs.LogGroup{
LogGroups: []cloudwatchlogstypes.LogGroup{
{Arn: utils.Pointer("arn:aws:logs:us-east-1:222:log-group:group_b"), LogGroupName: utils.Pointer("group_b")},
},
}, nil)
service := NewLogGroupsService(mockLogsAPI, false)
resp, err := service.GetLogGroupsWithContext(context.Background(), req)
resp, err := service.GetLogGroups(context.Background(), req)
assert.NoError(t, err)
mockLogsAPI.AssertNumberOfCalls(t, "DescribeLogGroupsWithContext", 2)
mockLogsAPI.AssertNumberOfCalls(t, "DescribeLogGroups", 2)
assert.Equal(t, []resources.ResourceResponse[resources.LogGroup]{
{
AccountId: utils.Pointer("111"),
@ -180,36 +183,36 @@ func TestGetLogGroups(t *testing.T) {
func TestGetLogGroupsCrossAccountQuerying(t *testing.T) {
t.Run("Should not includeLinkedAccounts or accountId if isCrossAccountEnabled is set to false", func(t *testing.T) {
mockLogsAPI := &mocks.LogsAPI{}
mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
service := NewLogGroupsService(mockLogsAPI, false)
_, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{
_, err := service.GetLogGroups(context.Background(), resources.LogGroupsRequest{
ResourceRequest: resources.ResourceRequest{AccountId: utils.Pointer("accountId")},
LogGroupNamePrefix: utils.Pointer("prefix"),
})
assert.NoError(t, err)
mockLogsAPI.AssertCalled(t, "DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{
Limit: utils.Pointer(int64(0)),
mockLogsAPI.AssertCalled(t, "DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{
Limit: aws.Int32(0),
LogGroupNamePrefix: utils.Pointer("prefix"),
})
})
t.Run("Should replace LogGroupNamePrefix if LogGroupNamePattern passed in resource call", func(t *testing.T) {
mockLogsAPI := &mocks.LogsAPI{}
mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
service := NewLogGroupsService(mockLogsAPI, true)
_, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{
_, err := service.GetLogGroups(context.Background(), resources.LogGroupsRequest{
ResourceRequest: resources.ResourceRequest{AccountId: utils.Pointer("accountId")},
LogGroupNamePrefix: utils.Pointer("prefix"),
LogGroupNamePattern: utils.Pointer("pattern"),
})
assert.NoError(t, err)
mockLogsAPI.AssertCalled(t, "DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{
AccountIdentifiers: []*string{utils.Pointer("accountId")},
Limit: utils.Pointer(int64(0)),
mockLogsAPI.AssertCalled(t, "DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{
AccountIdentifiers: []string{"accountId"},
Limit: aws.Int32(0),
LogGroupNamePrefix: utils.Pointer("pattern"),
IncludeLinkedAccounts: utils.Pointer(true),
})
@ -217,34 +220,34 @@ func TestGetLogGroupsCrossAccountQuerying(t *testing.T) {
t.Run("Should includeLinkedAccounts,and accountId if isCrossAccountEnabled is set to true", func(t *testing.T) {
mockLogsAPI := &mocks.LogsAPI{}
mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
service := NewLogGroupsService(mockLogsAPI, true)
_, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{
_, err := service.GetLogGroups(context.Background(), resources.LogGroupsRequest{
ResourceRequest: resources.ResourceRequest{AccountId: utils.Pointer("accountId")},
})
assert.NoError(t, err)
mockLogsAPI.AssertCalled(t, "DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{
Limit: utils.Pointer(int64(0)),
mockLogsAPI.AssertCalled(t, "DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{
Limit: aws.Int32(0),
IncludeLinkedAccounts: utils.Pointer(true),
AccountIdentifiers: []*string{utils.Pointer("accountId")},
AccountIdentifiers: []string{"accountId"},
})
})
t.Run("Should should not override prefix is there is no logGroupNamePattern", func(t *testing.T) {
mockLogsAPI := &mocks.LogsAPI{}
mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
service := NewLogGroupsService(mockLogsAPI, true)
_, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{
_, err := service.GetLogGroups(context.Background(), resources.LogGroupsRequest{
ResourceRequest: resources.ResourceRequest{AccountId: utils.Pointer("accountId")},
LogGroupNamePrefix: utils.Pointer("prefix"),
})
assert.NoError(t, err)
mockLogsAPI.AssertCalled(t, "DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{
AccountIdentifiers: []*string{utils.Pointer("accountId")},
Limit: utils.Pointer(int64(0)),
mockLogsAPI.AssertCalled(t, "DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{
AccountIdentifiers: []string{"accountId"},
Limit: aws.Int32(0),
LogGroupNamePrefix: utils.Pointer("prefix"),
IncludeLinkedAccounts: utils.Pointer(true),
})
@ -252,26 +255,26 @@ func TestGetLogGroupsCrossAccountQuerying(t *testing.T) {
t.Run("Should not includeLinkedAccounts, or accountId if accountId is nil", func(t *testing.T) {
mockLogsAPI := &mocks.LogsAPI{}
mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
service := NewLogGroupsService(mockLogsAPI, true)
_, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{
_, err := service.GetLogGroups(context.Background(), resources.LogGroupsRequest{
LogGroupNamePrefix: utils.Pointer("prefix"),
})
assert.NoError(t, err)
mockLogsAPI.AssertCalled(t, "DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{
Limit: utils.Pointer(int64(0)),
mockLogsAPI.AssertCalled(t, "DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{
Limit: aws.Int32(0),
LogGroupNamePrefix: utils.Pointer("prefix"),
})
})
t.Run("Should should not override prefix is there is no logGroupNamePattern", func(t *testing.T) {
mockLogsAPI := &mocks.LogsAPI{}
mockLogsAPI.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
mockLogsAPI.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{}, nil)
service := NewLogGroupsService(mockLogsAPI, true)
_, err := service.GetLogGroupsWithContext(context.Background(), resources.LogGroupsRequest{
_, err := service.GetLogGroups(context.Background(), resources.LogGroupsRequest{
ResourceRequest: resources.ResourceRequest{
AccountId: utils.Pointer("accountId"),
},
@ -279,10 +282,10 @@ func TestGetLogGroupsCrossAccountQuerying(t *testing.T) {
})
assert.NoError(t, err)
mockLogsAPI.AssertCalled(t, "DescribeLogGroupsWithContext", &cloudwatchlogs.DescribeLogGroupsInput{
AccountIdentifiers: []*string{utils.Pointer("accountId")},
mockLogsAPI.AssertCalled(t, "DescribeLogGroups", &cloudwatchlogs.DescribeLogGroupsInput{
AccountIdentifiers: []string{"accountId"},
IncludeLinkedAccounts: utils.Pointer(true),
Limit: utils.Pointer(int64(0)),
Limit: aws.Int32(0),
LogGroupNamePrefix: utils.Pointer("prefix"),
})
})
@ -291,24 +294,24 @@ func TestGetLogGroupsCrossAccountQuerying(t *testing.T) {
func TestGetLogGroupFields(t *testing.T) {
t.Run("Should map log group fields response", func(t *testing.T) {
mockLogsAPI := &mocks.LogsAPI{}
mockLogsAPI.On("GetLogGroupFieldsWithContext", mock.Anything).Return(
mockLogsAPI.On("GetLogGroupFields", mock.Anything).Return(
&cloudwatchlogs.GetLogGroupFieldsOutput{
LogGroupFields: []*cloudwatchlogs.LogGroupField{
LogGroupFields: []cloudwatchlogstypes.LogGroupField{
{
Name: utils.Pointer("field1"),
Percent: utils.Pointer(int64(10)),
Name: aws.String("field1"),
Percent: 10,
}, {
Name: utils.Pointer("field2"),
Percent: utils.Pointer(int64(10)),
Name: aws.String("field2"),
Percent: 10,
}, {
Name: utils.Pointer("field3"),
Percent: utils.Pointer(int64(10)),
Name: aws.String("field3"),
Percent: 10,
},
},
}, nil)
service := NewLogGroupsService(mockLogsAPI, false)
resp, err := service.GetLogGroupFieldsWithContext(context.Background(), resources.LogGroupFieldsRequest{})
resp, err := service.GetLogGroupFields(context.Background(), resources.LogGroupFieldsRequest{})
assert.NoError(t, err)
assert.Equal(t, []resources.ResourceResponse[resources.LogGroupField]{
@ -356,16 +359,16 @@ func TestGetLogGroupFields(t *testing.T) {
// remove this test once the above test is uncommented
t.Run("Should only set LogGroupName as api input in case both LogGroupName and LogGroupARN are specified", func(t *testing.T) {
mockLogsAPI := &mocks.LogsAPI{}
mockLogsAPI.On("GetLogGroupFieldsWithContext", mock.Anything).Return(
mockLogsAPI.On("GetLogGroupFields", mock.Anything).Return(
&cloudwatchlogs.GetLogGroupFieldsOutput{}, nil)
service := NewLogGroupsService(mockLogsAPI, false)
resp, err := service.GetLogGroupFieldsWithContext(context.Background(), resources.LogGroupFieldsRequest{
resp, err := service.GetLogGroupFields(context.Background(), resources.LogGroupFieldsRequest{
LogGroupName: "logGroupName",
LogGroupARN: "logGroupARN",
})
mockLogsAPI.AssertCalled(t, "GetLogGroupFieldsWithContext", &cloudwatchlogs.GetLogGroupFieldsInput{
mockLogsAPI.AssertCalled(t, "GetLogGroupFields", &cloudwatchlogs.GetLogGroupFieldsInput{
LogGroupIdentifier: nil,
LogGroupName: utils.Pointer("logGroupName"),
})
@ -375,16 +378,16 @@ func TestGetLogGroupFields(t *testing.T) {
t.Run("Should only set LogGroupName as api input in case only LogGroupName is specified", func(t *testing.T) {
mockLogsAPI := &mocks.LogsAPI{}
mockLogsAPI.On("GetLogGroupFieldsWithContext", mock.Anything).Return(
mockLogsAPI.On("GetLogGroupFields", mock.Anything).Return(
&cloudwatchlogs.GetLogGroupFieldsOutput{}, nil)
service := NewLogGroupsService(mockLogsAPI, false)
resp, err := service.GetLogGroupFieldsWithContext(context.Background(), resources.LogGroupFieldsRequest{
resp, err := service.GetLogGroupFields(context.Background(), resources.LogGroupFieldsRequest{
LogGroupName: "logGroupName",
LogGroupARN: "",
})
mockLogsAPI.AssertCalled(t, "GetLogGroupFieldsWithContext", &cloudwatchlogs.GetLogGroupFieldsInput{
mockLogsAPI.AssertCalled(t, "GetLogGroupFields", &cloudwatchlogs.GetLogGroupFieldsInput{
LogGroupIdentifier: nil,
LogGroupName: utils.Pointer("logGroupName"),
})

View File

@ -4,7 +4,9 @@ import (
"context"
"sort"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/constants"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
@ -16,14 +18,14 @@ type RegionsService struct {
log.Logger
}
func NewRegionsService(ec2client models.EC2APIProvider, logger log.Logger) models.RegionsAPIProvider {
var NewRegionsService = func(ec2client models.EC2APIProvider, logger log.Logger) models.RegionsAPIProvider {
return &RegionsService{
ec2client,
logger,
}
}
func mergeEC2RegionsAndConstantRegions(regions map[string]struct{}, ec2Regions []*ec2.Region) {
func mergeEC2RegionsAndConstantRegions(regions map[string]struct{}, ec2Regions []ec2types.Region) {
for _, region := range ec2Regions {
if _, ok := regions[*region.RegionName]; !ok {
regions[*region.RegionName] = struct{}{}
@ -36,7 +38,7 @@ func (r *RegionsService) GetRegions(ctx context.Context) ([]resources.ResourceRe
result := make([]resources.ResourceResponse[resources.Region], 0)
ec2Regions, err := r.DescribeRegionsWithContext(ctx, &ec2.DescribeRegionsInput{})
ec2Regions, err := r.DescribeRegions(ctx, &ec2.DescribeRegionsInput{})
// we ignore this error and always send default regions
// we only fetch incase a user has enabled additional regions
// but we still log it in case the user is expecting to fetch regions specific to their account and are unable to
@ -44,7 +46,9 @@ func (r *RegionsService) GetRegions(ctx context.Context) ([]resources.ResourceRe
r.Error("Failed to get regions: ", "error", err)
}
mergeEC2RegionsAndConstantRegions(regions, ec2Regions.Regions)
if ec2Regions != nil {
mergeEC2RegionsAndConstantRegions(regions, ec2Regions.Regions)
}
for region := range regions {
result = append(result, resources.ResourceResponse[resources.Region]{

View File

@ -4,7 +4,9 @@ import (
"context"
"testing"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
@ -17,14 +19,14 @@ var testLogger = log.New().With("logger", "test.logger")
func TestRegions(t *testing.T) {
t.Run("returns regions from the api and merges them with default regions", func(t *testing.T) {
mockRegions := &ec2.DescribeRegionsOutput{
Regions: []*ec2.Region{
Regions: []ec2types.Region{
{
RegionName: utils.Pointer("earth-1"),
},
},
}
ec2Mock := &mocks.EC2Mock{}
ec2Mock.On("DescribeRegionsWithContext").Return(mockRegions, nil)
ec2Mock.On("DescribeRegions").Return(mockRegions, nil)
regions, err := NewRegionsService(ec2Mock, testLogger).GetRegions(context.Background())
assert.NoError(t, err)
assert.Contains(t, regions, resources.ResourceResponse[resources.Region]{
@ -42,9 +44,9 @@ func TestRegions(t *testing.T) {
t.Run("always returns default regions, even if fetch fails", func(t *testing.T) {
ec2Mock := &mocks.EC2Mock{}
mockRegions := &ec2.DescribeRegionsOutput{
Regions: []*ec2.Region{},
Regions: []ec2types.Region{},
}
ec2Mock.On("DescribeRegionsWithContext").Return(mockRegions, assert.AnError)
ec2Mock.On("DescribeRegions").Return(mockRegions, assert.AnError)
regions, err := NewRegionsService(ec2Mock, testLogger).GetRegions(context.Background())
assert.NoError(t, err)
assert.Contains(t, regions, resources.ResourceResponse[resources.Region]{

View File

@ -6,7 +6,7 @@ import (
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
)
func valuesToListMetricRespone[T any](values []T) []resources.ResourceResponse[T] {
func valuesToListMetricResponse[T any](values []T) []resources.ResourceResponse[T] {
response := make([]resources.ResourceResponse[T], 0, len(values))
for _, value := range values {
response = append(response, resources.ResourceResponse[T]{Value: value})

View File

@ -5,7 +5,7 @@ import (
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/stretchr/testify/assert"

View File

@ -4,30 +4,24 @@ import (
"context"
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/aws/smithy-go"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
resourcegroupstaggingapitypes "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/patrickmn/go-cache"
"github.com/stretchr/testify/mock"
)
type fakeCWLogsClient struct {
cloudwatchlogsiface.CloudWatchLogsAPI
calls logsQueryCalls
logGroups []cloudwatchlogs.DescribeLogGroupsOutput
@ -38,83 +32,104 @@ type fakeCWLogsClient struct {
}
type logsQueryCalls struct {
startQueryWithContext []*cloudwatchlogs.StartQueryInput
getEventsWithContext []*cloudwatchlogs.GetLogEventsInput
describeLogGroups []*cloudwatchlogs.DescribeLogGroupsInput
startQuery []*cloudwatchlogs.StartQueryInput
getEvents []*cloudwatchlogs.GetLogEventsInput
describeLogGroups []*cloudwatchlogs.DescribeLogGroupsInput
}
func (m *fakeCWLogsClient) GetQueryResultsWithContext(ctx context.Context, input *cloudwatchlogs.GetQueryResultsInput, option ...request.Option) (*cloudwatchlogs.GetQueryResultsOutput, error) {
func (m *fakeCWLogsClient) GetQueryResults(_ context.Context, _ *cloudwatchlogs.GetQueryResultsInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetQueryResultsOutput, error) {
return &m.queryResults, nil
}
func (m *fakeCWLogsClient) StartQueryWithContext(ctx context.Context, input *cloudwatchlogs.StartQueryInput, option ...request.Option) (*cloudwatchlogs.StartQueryOutput, error) {
m.calls.startQueryWithContext = append(m.calls.startQueryWithContext, input)
func (m *fakeCWLogsClient) StartQuery(_ context.Context, input *cloudwatchlogs.StartQueryInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.StartQueryOutput, error) {
m.calls.startQuery = append(m.calls.startQuery, input)
return &cloudwatchlogs.StartQueryOutput{
QueryId: aws.String("abcd-efgh-ijkl-mnop"),
}, nil
}
func (m *fakeCWLogsClient) StopQueryWithContext(ctx context.Context, input *cloudwatchlogs.StopQueryInput, option ...request.Option) (*cloudwatchlogs.StopQueryOutput, error) {
func (m *fakeCWLogsClient) StopQuery(_ context.Context, _ *cloudwatchlogs.StopQueryInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.StopQueryOutput, error) {
return &cloudwatchlogs.StopQueryOutput{
Success: aws.Bool(true),
Success: true,
}, nil
}
type mockLogsSyncClient struct {
cloudwatchlogsiface.CloudWatchLogsAPI
mock.Mock
}
func (m *mockLogsSyncClient) GetQueryResultsWithContext(ctx context.Context, input *cloudwatchlogs.GetQueryResultsInput, option ...request.Option) (*cloudwatchlogs.GetQueryResultsOutput, error) {
args := m.Called(ctx, input, option)
func (m *mockLogsSyncClient) StopQuery(context.Context, *cloudwatchlogs.StopQueryInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.StopQueryOutput, error) {
return nil, nil
}
func (m *mockLogsSyncClient) GetLogEvents(context.Context, *cloudwatchlogs.GetLogEventsInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetLogEventsOutput, error) {
return nil, nil
}
func (m *mockLogsSyncClient) DescribeLogGroups(context.Context, *cloudwatchlogs.DescribeLogGroupsInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
return nil, nil
}
func (m *mockLogsSyncClient) GetQueryResults(ctx context.Context, input *cloudwatchlogs.GetQueryResultsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetQueryResultsOutput, error) {
args := m.Called(ctx, input, optFns)
return args.Get(0).(*cloudwatchlogs.GetQueryResultsOutput), args.Error(1)
}
func (m *mockLogsSyncClient) StartQueryWithContext(ctx context.Context, input *cloudwatchlogs.StartQueryInput, option ...request.Option) (*cloudwatchlogs.StartQueryOutput, error) {
args := m.Called(ctx, input, option)
func (m *mockLogsSyncClient) StartQuery(ctx context.Context, input *cloudwatchlogs.StartQueryInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.StartQueryOutput, error) {
args := m.Called(ctx, input, optFns)
return args.Get(0).(*cloudwatchlogs.StartQueryOutput), args.Error(1)
}
func (m *fakeCWLogsClient) DescribeLogGroupsWithContext(ctx context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, option ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
func (m *fakeCWLogsClient) DescribeLogGroups(_ context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
m.calls.describeLogGroups = append(m.calls.describeLogGroups, input)
output := &m.logGroups[m.logGroupsIndex]
m.logGroupsIndex++
return output, nil
}
func (m *fakeCWLogsClient) GetLogGroupFieldsWithContext(ctx context.Context, input *cloudwatchlogs.GetLogGroupFieldsInput, option ...request.Option) (*cloudwatchlogs.GetLogGroupFieldsOutput, error) {
func (m *fakeCWLogsClient) GetLogGroupFields(_ context.Context, _ *cloudwatchlogs.GetLogGroupFieldsInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetLogGroupFieldsOutput, error) {
return &m.logGroupFields, nil
}
func (m *fakeCWLogsClient) GetLogEventsWithContext(ctx context.Context, input *cloudwatchlogs.GetLogEventsInput, option ...request.Option) (*cloudwatchlogs.GetLogEventsOutput, error) {
m.calls.getEventsWithContext = append(m.calls.getEventsWithContext, input)
func (m *fakeCWLogsClient) GetLogEvents(_ context.Context, input *cloudwatchlogs.GetLogEventsInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetLogEventsOutput, error) {
m.calls.getEvents = append(m.calls.getEvents, input)
return &cloudwatchlogs.GetLogEventsOutput{
Events: []*cloudwatchlogs.OutputLogEvent{},
Events: []cloudwatchlogstypes.OutputLogEvent{},
}, nil
}
type fakeCWAnnotationsClient struct {
cloudwatchiface.CloudWatchAPI
calls annontationsQueryCalls
describeAlarmsForMetricOutput *cloudwatch.DescribeAlarmsForMetricOutput
describeAlarmsOutput *cloudwatch.DescribeAlarmsOutput
}
func (c *fakeCWAnnotationsClient) DescribeAlarmHistory(ctx context.Context, input *cloudwatch.DescribeAlarmHistoryInput, f ...func(*cloudwatch.Options)) (*cloudwatch.DescribeAlarmHistoryOutput, error) {
return nil, nil
}
func (c *fakeCWAnnotationsClient) GetMetricData(ctx context.Context, input *cloudwatch.GetMetricDataInput, f ...func(*cloudwatch.Options)) (*cloudwatch.GetMetricDataOutput, error) {
return nil, nil
}
func (c *fakeCWAnnotationsClient) ListMetrics(ctx context.Context, input *cloudwatch.ListMetricsInput, f ...func(*cloudwatch.Options)) (*cloudwatch.ListMetricsOutput, error) {
return nil, nil
}
type annontationsQueryCalls struct {
describeAlarmsForMetric []*cloudwatch.DescribeAlarmsForMetricInput
describeAlarms []*cloudwatch.DescribeAlarmsInput
}
func (c *fakeCWAnnotationsClient) DescribeAlarmsForMetric(params *cloudwatch.DescribeAlarmsForMetricInput) (*cloudwatch.DescribeAlarmsForMetricOutput, error) {
func (c *fakeCWAnnotationsClient) DescribeAlarmsForMetric(_ context.Context, params *cloudwatch.DescribeAlarmsForMetricInput, _ ...func(*cloudwatch.Options)) (*cloudwatch.DescribeAlarmsForMetricOutput, error) {
c.calls.describeAlarmsForMetric = append(c.calls.describeAlarmsForMetric, params)
return c.describeAlarmsForMetricOutput, nil
}
func (c *fakeCWAnnotationsClient) DescribeAlarms(params *cloudwatch.DescribeAlarmsInput) (*cloudwatch.DescribeAlarmsOutput, error) {
func (c *fakeCWAnnotationsClient) DescribeAlarms(_ context.Context, params *cloudwatch.DescribeAlarmsInput, _ ...func(*cloudwatch.Options)) (*cloudwatch.DescribeAlarmsOutput, error) {
c.calls.describeAlarms = append(c.calls.describeAlarms, params)
return c.describeAlarmsOutput, nil
@ -122,16 +137,14 @@ func (c *fakeCWAnnotationsClient) DescribeAlarms(params *cloudwatch.DescribeAlar
// Please use mockEC2Client above, we are slowly migrating towards using testify's mocks only
type oldEC2Client struct {
ec2iface.EC2API
regions []string
reservations []*ec2.Reservation
reservations []ec2types.Reservation
}
func (c oldEC2Client) DescribeRegionsWithContext(ctx aws.Context, in *ec2.DescribeRegionsInput, option ...request.Option) (*ec2.DescribeRegionsOutput, error) {
regions := []*ec2.Region{}
func (c oldEC2Client) DescribeRegions(_ context.Context, _ *ec2.DescribeRegionsInput, _ ...func(*ec2.Options)) (*ec2.DescribeRegionsOutput, error) {
regions := []ec2types.Region{}
for _, region := range c.regions {
regions = append(regions, &ec2.Region{
regions = append(regions, ec2types.Region{
RegionName: aws.String(region),
})
}
@ -140,11 +153,10 @@ func (c oldEC2Client) DescribeRegionsWithContext(ctx aws.Context, in *ec2.Descri
}, nil
}
func (c oldEC2Client) DescribeInstancesPagesWithContext(ctx aws.Context, in *ec2.DescribeInstancesInput,
fn func(*ec2.DescribeInstancesOutput, bool) bool, opts ...request.Option) error {
reservations := []*ec2.Reservation{}
func (c oldEC2Client) DescribeInstances(_ context.Context, in *ec2.DescribeInstancesInput, _ ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
reservations := []ec2types.Reservation{}
for _, r := range c.reservations {
instances := []*ec2.Instance{}
instances := []ec2types.Instance{}
for _, inst := range r.Instances {
if len(in.InstanceIds) == 0 {
instances = append(instances, inst)
@ -152,97 +164,59 @@ func (c oldEC2Client) DescribeInstancesPagesWithContext(ctx aws.Context, in *ec2
}
for _, id := range in.InstanceIds {
if *inst.InstanceId == *id {
if *inst.InstanceId == id {
instances = append(instances, inst)
}
}
}
reservation := &ec2.Reservation{Instances: instances}
reservation := ec2types.Reservation{Instances: instances}
reservations = append(reservations, reservation)
}
fn(&ec2.DescribeInstancesOutput{
return &ec2.DescribeInstancesOutput{
Reservations: reservations,
}, true)
return nil
}, nil
}
type fakeRGTAClient struct {
resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI
tagMapping []*resourcegroupstaggingapi.ResourceTagMapping
tagMapping []resourcegroupstaggingapitypes.ResourceTagMapping
}
func (c fakeRGTAClient) GetResourcesPagesWithContext(ctx context.Context, in *resourcegroupstaggingapi.GetResourcesInput,
fn func(*resourcegroupstaggingapi.GetResourcesOutput, bool) bool, opts ...request.Option) error {
fn(&resourcegroupstaggingapi.GetResourcesOutput{
func (c fakeRGTAClient) GetResources(_ context.Context, _ *resourcegroupstaggingapi.GetResourcesInput, _ ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
return &resourcegroupstaggingapi.GetResourcesOutput{
ResourceTagMappingList: c.tagMapping,
}, true)
return nil
}, nil
}
type fakeCheckHealthClient struct {
listMetricsPages func(input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool) error
describeLogGroups func(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error)
listMetricsFunction func(context.Context, *cloudwatch.ListMetricsInput, ...func(*cloudwatch.Options)) (*cloudwatch.ListMetricsOutput, error)
describeLogGroupsFunction func(context.Context, *cloudwatchlogs.DescribeLogGroupsInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error)
models.CWClient
}
func (c fakeCheckHealthClient) ListMetricsPagesWithContext(ctx aws.Context, input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool, opts ...request.Option) error {
if c.listMetricsPages != nil {
return c.listMetricsPages(input, fn)
func (c fakeCheckHealthClient) ListMetrics(ctx context.Context, input *cloudwatch.ListMetricsInput, _ ...func(*cloudwatch.Options)) (*cloudwatch.ListMetricsOutput, error) {
if c.listMetricsFunction != nil {
return c.listMetricsFunction(ctx, input)
}
return nil
return &cloudwatch.ListMetricsOutput{}, nil
}
func (c fakeCheckHealthClient) DescribeLogGroupsWithContext(ctx context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, option ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
if c.describeLogGroups != nil {
return c.describeLogGroups(input)
func (c fakeCheckHealthClient) DescribeLogGroups(ctx context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
if c.describeLogGroupsFunction != nil {
return c.describeLogGroupsFunction(ctx, input)
}
return nil, nil
}
func (c fakeCheckHealthClient) GetLogGroupFieldsWithContext(ctx context.Context, input *cloudwatchlogs.GetLogGroupFieldsInput, option ...request.Option) (*cloudwatchlogs.GetLogGroupFieldsOutput, error) {
func (c fakeCheckHealthClient) GetLogGroupFields(_ context.Context, _ *cloudwatchlogs.GetLogGroupFieldsInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetLogGroupFieldsOutput, error) {
return nil, nil
}
func testInstanceManager(pageLimit int) instancemgmt.InstanceManager {
return datasource.NewInstanceManager((func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{
AWSDatasourceSettings: awsds.AWSDatasourceSettings{
Region: "us-east-1",
},
GrafanaSettings: awsds.AuthSettings{ListMetricsPageLimit: pageLimit},
},
sessions: &fakeSessionCache{},
tagValueCache: cache.New(0, 0)}, nil
}))
type FakeCredentialsProvider struct {
}
func defaultTestInstanceManager() instancemgmt.InstanceManager {
return testInstanceManager(1000)
}
type mockSessionCache struct {
mock.Mock
}
func (c *mockSessionCache) GetSessionWithAuthSettings(config awsds.GetSessionConfig, auth awsds.AuthSettings) (*session.Session, error) {
args := c.Called(config)
return args.Get(0).(*session.Session), args.Error(1)
}
type fakeSessionCache struct {
getSessionWithAuthSettings func(c awsds.GetSessionConfig, a awsds.AuthSettings) (*session.Session, error)
calledRegions []string
}
func (s *fakeSessionCache) GetSessionWithAuthSettings(c awsds.GetSessionConfig, a awsds.AuthSettings) (*session.Session, error) {
s.calledRegions = append(s.calledRegions, c.Settings.Region)
if s.getSessionWithAuthSettings != nil {
return s.getSessionWithAuthSettings(c, a)
}
return &session.Session{
Config: &aws.Config{},
}, nil
func (fcp *FakeCredentialsProvider) Retrieve(_ context.Context) (aws.Credentials, error) {
return aws.Credentials{}, nil
}
type mockedCallResourceResponseSenderForOauth struct {
@ -254,25 +228,25 @@ func (s *mockedCallResourceResponseSenderForOauth) Send(resp *backend.CallResour
return nil
}
type fakeAWSError struct {
type fakeSmithyError struct {
code string
message string
}
func (e fakeAWSError) OrigErr() error {
return nil
func (f fakeSmithyError) Error() string {
return f.message
}
func (e fakeAWSError) Error() string {
return e.message
func (f fakeSmithyError) ErrorCode() string {
return f.code
}
func (e fakeAWSError) Code() string {
return e.code
func (f fakeSmithyError) ErrorMessage() string {
return f.message
}
func (e fakeAWSError) Message() string {
return e.message
func (f fakeSmithyError) ErrorFault() smithy.ErrorFault {
return 0
}
func contextWithFeaturesEnabled(enabled ...string) context.Context {

View File

@ -18,20 +18,14 @@ type responseWrapper struct {
RefId string
}
func (e *cloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
e.logger.FromContext(ctx).Debug("Executing time series query")
func (ds *DataSource) executeTimeSeriesQuery(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
ds.logger.FromContext(ctx).Debug("Executing time series query")
resp := backend.NewQueryDataResponse()
if len(req.Queries) == 0 {
return nil, backend.DownstreamError(fmt.Errorf("request contains no queries"))
}
instance, err := e.getInstance(ctx, req.PluginContext)
if err != nil {
resp.Responses[req.Queries[0].RefID] = backend.ErrorResponseWithErrorSource(err)
return resp, nil
}
timeBatches := utils.BatchDataQueriesByTimeRange(req.Queries)
requestQueriesByTimeAndRegion := make(map[string][]*models.CloudWatchQuery)
for i, timeBatch := range timeBatches {
@ -40,7 +34,7 @@ func (e *cloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, req *ba
if !startTime.Before(endTime) {
return nil, backend.DownstreamError(fmt.Errorf("invalid time range: start time must be before end time"))
}
requestQueries, err := models.ParseMetricDataQueries(timeBatch, startTime, endTime, instance.Settings.Region, e.logger.FromContext(ctx),
requestQueries, err := models.ParseMetricDataQueries(timeBatch, startTime, endTime, ds.Settings.Region, ds.logger.FromContext(ctx),
features.IsEnabled(ctx, features.FlagCloudWatchCrossAccountQuerying))
if err != nil {
return nil, err
@ -63,7 +57,7 @@ func (e *cloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, req *ba
for _, timeAndRegionQueries := range requestQueriesByTimeAndRegion {
batches := [][]*models.CloudWatchQuery{timeAndRegionQueries}
if features.IsEnabled(ctx, features.FlagCloudWatchBatchQueries) {
batches = getMetricQueryBatches(timeAndRegionQueries, e.logger.FromContext(ctx))
batches = getMetricQueryBatches(timeAndRegionQueries, ds.logger.FromContext(ctx))
}
// region, startTime, and endTime are the same for the set of queries
@ -76,7 +70,7 @@ func (e *cloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, req *ba
eg.Go(func() error {
defer func() {
if err := recover(); err != nil {
e.logger.FromContext(ctx).Error("Execute Get Metric Data Query Panic", "error", err, "stack", utils.Stack(1))
ds.logger.FromContext(ctx).Error("Execute Get Metric Data Query Panic", "error", err, "stack", utils.Stack(1))
if theErr, ok := err.(error); ok {
resultChan <- &responseWrapper{
DataResponse: &backend.DataResponse{
@ -87,27 +81,27 @@ func (e *cloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, req *ba
}
}()
client, err := e.getCWClient(ctx, req.PluginContext, region)
client, err := ds.getCWClient(ctx, region)
if err != nil {
return err
}
metricDataInput, err := e.buildMetricDataInput(ctx, startTime, endTime, requestQueries)
metricDataInput, err := ds.buildMetricDataInput(ctx, startTime, endTime, requestQueries)
if err != nil {
return err
}
mdo, err := e.executeRequest(ectx, client, metricDataInput)
mdo, err := ds.executeRequest(ectx, client, metricDataInput)
if err != nil {
return err
}
requestQueries, err = e.getDimensionValuesForWildcards(ctx, region, client, requestQueries, instance.tagValueCache, instance.Settings.GrafanaSettings.ListMetricsPageLimit, shouldSkipFetchingWildcards)
requestQueries, err = ds.getDimensionValuesForWildcards(ctx, region, client, requestQueries, ds.tagValueCache, ds.Settings.GrafanaSettings.ListMetricsPageLimit, shouldSkipFetchingWildcards)
if err != nil {
return err
}
res, err := e.parseResponse(ctx, mdo, requestQueries)
res, err := ds.parseResponse(ctx, mdo, requestQueries)
if err != nil {
return err
}

View File

@ -6,16 +6,11 @@ import (
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/stretchr/testify/mock"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/kinds/dataquery"
@ -23,11 +18,12 @@ import (
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestTimeSeriesQuery(t *testing.T) {
executor := newExecutor(defaultTestInstanceManager(), log.NewNullLogger())
ds := newTestDatasource()
now := time.Now()
origNewCWClient := NewCWClient
@ -36,25 +32,22 @@ func TestTimeSeriesQuery(t *testing.T) {
})
var api mocks.MetricsAPI
NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI {
NewCWClient = func(aws.Config) models.CWClient {
return &api
}
t.Run("Custom metrics", func(t *testing.T) {
api = mocks.MetricsAPI{}
api.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{
MetricDataResults: []*cloudwatch.MetricDataResult{
api.On("GetMetricData", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{
MetricDataResults: []cloudwatchtypes.MetricDataResult{
{
StatusCode: aws.String("Complete"), Id: aws.String("a"), Label: aws.String("NetworkOut"), Values: []*float64{aws.Float64(1.0)}, Timestamps: []*time.Time{&now},
StatusCode: "Complete", Id: aws.String("a"), Label: aws.String("NetworkOut"), Values: []float64{1.0}, Timestamps: []time.Time{now},
},
{
StatusCode: aws.String("Complete"), Id: aws.String("b"), Label: aws.String("NetworkIn"), Values: []*float64{aws.Float64(1.0)}, Timestamps: []*time.Time{&now},
StatusCode: "Complete", Id: aws.String("b"), Label: aws.String("NetworkIn"), Values: []float64{1.0}, Timestamps: []time.Time{now},
}}}, nil)
im := defaultTestInstanceManager()
executor := newExecutor(im, log.NewNullLogger())
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
resp, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
@ -118,7 +111,7 @@ func TestTimeSeriesQuery(t *testing.T) {
})
t.Run("End time before start time should result in error", func(t *testing.T) {
_, err := executor.executeTimeSeriesQuery(context.Background(), &backend.QueryDataRequest{
_, err := ds.executeTimeSeriesQuery(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
@ -130,7 +123,7 @@ func TestTimeSeriesQuery(t *testing.T) {
})
t.Run("End time equals start time should result in error", func(t *testing.T) {
_, err := executor.executeTimeSeriesQuery(context.Background(), &backend.QueryDataRequest{
_, err := ds.executeTimeSeriesQuery(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
@ -145,31 +138,26 @@ func TestTimeSeriesQuery(t *testing.T) {
func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMetricData_is_called_once_per_grouping_of_queries_by_region(t *testing.T) {
/* TODO: This test aims to verify the logic to group regions which has been extracted from ParseMetricDataQueries.
It should be replaced by a test at a lower level when grouping by regions is incorporated into a separate business logic layer */
// FIXME: this test is broken - it only works because we're recovering from the panic that the Mock
// produces - see time_series_query.go line 78. If that recover is commented out, the test fails.
t.Skip("skipping broken test")
ds := newTestDatasource()
origNewCWClient := NewCWClient
t.Cleanup(func() {
NewCWClient = origNewCWClient
})
var mockMetricClient mocks.MetricsAPI
NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI {
NewCWClient = func(aws.Config) models.CWClient {
return &mockMetricClient
}
t.Run("Queries with the same region should call GetSessionWithAuthSettings with that region 1 time and call GetMetricDataWithContext 1 time", func(t *testing.T) {
mockSessionCache := &mockSessionCache{}
mockSessionCache.On("GetSessionWithAuthSettings", mock.MatchedBy(
func(config awsds.GetSessionConfig) bool {
return config.Settings.Region == "us-east-1"
})). // region from queries is asserted here
Return(&session.Session{Config: &aws.Config{}}, nil).Once()
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: mockSessionCache}, nil
})
t.Run("Queries with the same region should call GetMetricData 1 time", func(t *testing.T) {
mockMetricClient = mocks.MetricsAPI{}
mockMetricClient.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
mockMetricClient.On("GetMetricData", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil)
executor := newExecutor(im, log.NewNullLogger())
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
@ -202,34 +190,16 @@ func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMe
})
require.NoError(t, err)
mockSessionCache.AssertExpectations(t) // method is defined to only return "Once()",
// AssertExpectations will fail if those methods were not called Once(), so expected number of calls is asserted by this line
mockMetricClient.AssertNumberOfCalls(t, "GetMetricDataWithContext", 1)
mockMetricClient.AssertNumberOfCalls(t, "GetMetricData", 1)
// GetMetricData is asserted to have been called 1 time for the 1 region present in the queries
})
t.Run("3 queries with 2 regions calls GetSessionWithAuthSettings 2 times and calls GetMetricDataWithContext 2 times", func(t *testing.T) {
sessionCache := &mockSessionCache{}
sessionCache.On("GetSessionWithAuthSettings", mock.MatchedBy(
func(config awsds.GetSessionConfig) bool {
return config.Settings.Region == "us-east-1"
})).
Return(&session.Session{Config: &aws.Config{}}, nil).Once()
sessionCache.On("GetSessionWithAuthSettings", mock.MatchedBy(
func(config awsds.GetSessionConfig) bool {
return config.Settings.Region == "us-east-2"
})).
Return(&session.Session{Config: &aws.Config{}}, nil).Once()
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: sessionCache}, nil
})
t.Run("3 queries with 2 regions calls GetMetricData 2 times", func(t *testing.T) {
mockMetricClient = mocks.MetricsAPI{}
mockMetricClient.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
mockMetricClient.On("GetMetricData", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil)
executor := newExecutor(im, log.NewNullLogger())
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
@ -274,29 +244,16 @@ func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMe
})
require.NoError(t, err)
sessionCache.AssertExpectations(t) // method is defined to only return "Once()" for each region.
// AssertExpectations will fail if those methods were not called Once(), so expected number of calls is asserted by this line
mockMetricClient.AssertNumberOfCalls(t, "GetMetricDataWithContext", 2)
mockMetricClient.AssertNumberOfCalls(t, "GetMetricData", 2)
// GetMetricData is asserted to have been called 2 times, presumably once for each group of regions (2 regions total)
})
t.Run("3 queries with 2 time ranges calls GetSessionWithAuthSettings 2 times and calls GetMetricDataWithContext 2 times", func(t *testing.T) {
sessionCache := &mockSessionCache{}
sessionCache.On("GetSessionWithAuthSettings", mock.MatchedBy(
func(config awsds.GetSessionConfig) bool {
return config.Settings.Region == "us-east-2"
})).
Return(&session.Session{Config: &aws.Config{}}, nil).Times(2)
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: sessionCache}, nil
})
t.Run("3 queries with 2 time ranges calls GetMetricData 2 times", func(t *testing.T) {
mockMetricClient = mocks.MetricsAPI{}
mockMetricClient.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
mockMetricClient.On("GetMetricData", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil)
executor := newExecutor(im, log.NewNullLogger())
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
@ -341,8 +298,7 @@ func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMe
})
require.NoError(t, err)
sessionCache.AssertExpectations(t) // method is defined to return twice (once for each batch)
mockMetricClient.AssertNumberOfCalls(t, "GetMetricDataWithContext", 2)
mockMetricClient.AssertNumberOfCalls(t, "GetMetricData", 2)
// GetMetricData is asserted to have been called 2 times, presumably once for each time range (2 time ranges total)
})
}
@ -408,7 +364,9 @@ func newTestQuery(t testing.TB, p queryParameters) json.RawMessage {
return marshalled
}
func Test_QueryData_timeSeriesQuery_GetMetricDataWithContext(t *testing.T) {
func Test_QueryData_timeSeriesQuery_GetMetricData(t *testing.T) {
ds := newTestDatasource()
origNewCWClient := NewCWClient
t.Cleanup(func() {
NewCWClient = origNewCWClient
@ -416,23 +374,18 @@ func Test_QueryData_timeSeriesQuery_GetMetricDataWithContext(t *testing.T) {
var api mocks.MetricsAPI
NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI {
NewCWClient = func(aws.Config) models.CWClient {
return &api
}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
})
t.Run("passes query label as GetMetricData label", func(t *testing.T) {
api = mocks.MetricsAPI{}
api.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil)
executor := newExecutor(im, log.NewNullLogger())
api.On("GetMetricData", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil)
query := newTestQuery(t, queryParameters{
Label: aws.String("${PROP('Period')} some words ${PROP('Dim.InstanceId')}"),
})
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
{
@ -465,10 +418,8 @@ func Test_QueryData_timeSeriesQuery_GetMetricDataWithContext(t *testing.T) {
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
api = mocks.MetricsAPI{}
api.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil)
executor := newExecutor(im, log.NewNullLogger())
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
api.On("GetMetricData", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil)
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
{
@ -494,31 +445,30 @@ func Test_QueryData_timeSeriesQuery_GetMetricDataWithContext(t *testing.T) {
}
func Test_QueryData_response_data_frame_name_is_always_response_label(t *testing.T) {
ds := newTestDatasource()
origNewCWClient := NewCWClient
t.Cleanup(func() {
NewCWClient = origNewCWClient
})
api := mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{
{MetricName: aws.String(""), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("InstanceId"), Value: aws.String("i-00645d91ed77d87ac")}}},
api := mocks.MetricsAPI{Metrics: []cloudwatchtypes.Metric{
{MetricName: aws.String(""), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("InstanceId"), Value: aws.String("i-00645d91ed77d87ac")}}},
}}
api.On("ListMetricsPagesWithContext").Return(nil)
api.On("ListMetricsPages").Return(nil)
NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI {
NewCWClient = func(aws.Config) models.CWClient {
return &api
}
labelFromGetMetricData := "some label"
api.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).
api.On("GetMetricData", mock.Anything, mock.Anything, mock.Anything).
Return(&cloudwatch.GetMetricDataOutput{
MetricDataResults: []*cloudwatch.MetricDataResult{
{StatusCode: aws.String("Complete"), Id: aws.String(queryId), Label: aws.String(labelFromGetMetricData),
Values: []*float64{aws.Float64(1.0)}, Timestamps: []*time.Time{{}}},
MetricDataResults: []cloudwatchtypes.MetricDataResult{
{StatusCode: "Complete", Id: aws.String(queryId), Label: aws.String(labelFromGetMetricData),
Values: []float64{1.0}, Timestamps: []time.Time{{}}},
}}, nil)
im := defaultTestInstanceManager()
executor := newExecutor(im, log.NewNullLogger())
t.Run("where user defines search expression", func(t *testing.T) {
query := newTestQuery(t, queryParameters{
MetricQueryType: models.MetricQueryTypeSearch, // contributes to isUserDefinedSearchExpression = true
@ -528,7 +478,7 @@ func Test_QueryData_response_data_frame_name_is_always_response_label(t *testing
Period: "1200", // period parsed from expression takes precedence over 1200
})
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
resp, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
{
@ -549,7 +499,7 @@ func Test_QueryData_response_data_frame_name_is_always_response_label(t *testing
MetricEditorMode: models.MetricEditorModeRaw,
})
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
resp, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
{
@ -569,7 +519,7 @@ func Test_QueryData_response_data_frame_name_is_always_response_label(t *testing
MetricQueryType: models.MetricQueryTypeQuery,
})
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
resp, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
{
@ -595,7 +545,7 @@ func Test_QueryData_response_data_frame_name_is_always_response_label(t *testing
t.Run(name, func(t *testing.T) {
query := newTestQuery(t, parameters)
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
resp, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
{
@ -642,7 +592,7 @@ func Test_QueryData_response_data_frame_name_is_always_response_label(t *testing
t.Run(name, func(t *testing.T) {
query := newTestQuery(t, parameters)
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
resp, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
Queries: []backend.DataQuery{
{
@ -660,23 +610,23 @@ func Test_QueryData_response_data_frame_name_is_always_response_label(t *testing
}
func TestTimeSeriesQuery_CrossAccountQuerying(t *testing.T) {
ds := newTestDatasource()
origNewCWClient := NewCWClient
t.Cleanup(func() {
NewCWClient = origNewCWClient
})
var api mocks.MetricsAPI
NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI {
NewCWClient = func(aws.Config) models.CWClient {
return &api
}
im := defaultTestInstanceManager()
t.Run("should call GetMetricDataInput with AccountId nil when no AccountId is provided", func(t *testing.T) {
api = mocks.MetricsAPI{}
api.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil)
executor := newExecutor(im, log.NewNullLogger())
api.On("GetMetricData", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil)
_, err := executor.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
_, err := ds.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
@ -714,9 +664,8 @@ func TestTimeSeriesQuery_CrossAccountQuerying(t *testing.T) {
t.Run("should call GetMetricDataInput with AccountId nil when feature flag is false", func(t *testing.T) {
api = mocks.MetricsAPI{}
api.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil)
executor := newExecutor(im, log.NewNullLogger())
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
api.On("GetMetricData", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil)
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
@ -755,9 +704,8 @@ func TestTimeSeriesQuery_CrossAccountQuerying(t *testing.T) {
t.Run("should call GetMetricDataInput with AccountId in a MetricStat query", func(t *testing.T) {
api = mocks.MetricsAPI{}
api.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil)
executor := newExecutor(im, log.NewNullLogger())
_, err := executor.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
api.On("GetMetricData", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil)
_, err := ds.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},
@ -796,9 +744,8 @@ func TestTimeSeriesQuery_CrossAccountQuerying(t *testing.T) {
t.Run("should GetMetricDataInput with AccountId in an inferred search expression query", func(t *testing.T) {
api = mocks.MetricsAPI{}
api.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil)
executor := newExecutor(im, log.NewNullLogger())
_, err := executor.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
api.On("GetMetricData", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatch.GetMetricDataOutput{}, nil)
_, err := ds.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
},

View File

@ -6,12 +6,7 @@ import (
"github.com/prometheus/client_golang/prometheus/promauto"
)
const (
// Labels for the metric counter query types
ListMetricsLabel = "list_metrics"
GetMetricDataLabel = "get_metric_data"
)
const GetMetricDataLabel = "get_metric_data"
var QueriesTotalCounter = promauto.NewCounterVec(
prometheus.CounterOpts{

View File

@ -4,6 +4,7 @@ import { QueryEditorProps } from '@grafana/data';
import { CloudWatchDatasource } from '../../datasource';
import { isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from '../../guards';
import useMigratedQuery from '../../migrations/useMigratedQuery';
import { CloudWatchJsonData, CloudWatchQuery } from '../../types';
import LogsQueryEditor from './LogsQueryEditor/LogsQueryEditor';
@ -14,6 +15,7 @@ export type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, Clou
export const QueryEditor = (props: Props) => {
const { query, onChange, data } = props;
const migratedQuery = useMigratedQuery(query, props.onChange);
const [dataIsStale, setDataIsStale] = useState(false);
const [extraHeaderElementLeft, setExtraHeaderElementLeft] = useState<JSX.Element>();
const [extraHeaderElementRight, setExtraHeaderElementRight] = useState<JSX.Element>();
@ -39,20 +41,20 @@ export const QueryEditor = (props: Props) => {
dataIsStale={dataIsStale}
/>
{isCloudWatchMetricsQuery(query) && (
{isCloudWatchMetricsQuery(migratedQuery) && (
<MetricsQueryEditor
{...props}
query={query}
query={migratedQuery}
onRunQuery={() => {}}
onChange={onChangeInternal}
extraHeaderElementLeft={setExtraHeaderElementLeft}
extraHeaderElementRight={setExtraHeaderElementRight}
/>
)}
{isCloudWatchLogsQuery(query) && (
{isCloudWatchLogsQuery(migratedQuery) && (
<LogsQueryEditor
{...props}
query={query}
query={migratedQuery}
onChange={onChangeInternal}
extraHeaderElementLeft={setExtraHeaderElementLeft}
/>

Some files were not shown because too many files have changed in this diff Show More