diff --git a/go.mod b/go.mod index e6572a67c9f..95d0e895bdd 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,9 @@ module github.com/grafana/grafana go 1.14 require ( - cloud.google.com/go v0.38.0 // indirect github.com/BurntSushi/toml v0.3.1 github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f - github.com/aws/aws-sdk-go v1.25.48 + github.com/aws/aws-sdk-go v1.29.20 github.com/beevik/etree v1.1.0 // indirect github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3 github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 @@ -23,7 +22,7 @@ require ( github.com/go-macaron/binding v0.0.0-20190806013118-0b4f37bab25b github.com/go-macaron/gzip v0.0.0-20160222043647-cad1c6580a07 github.com/go-macaron/session v0.0.0-20190805070824-1a3cdc6f5659 - github.com/go-sql-driver/mysql v1.4.1 + github.com/go-sql-driver/mysql v1.5.0 github.com/go-stack/stack v1.8.0 github.com/gobwas/glob v0.2.3 github.com/golang/protobuf v1.3.4 @@ -48,7 +47,7 @@ require ( github.com/mattn/go-sqlite3 v1.11.0 github.com/opentracing/opentracing-go v1.1.0 github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/pkg/errors v0.8.1 + github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.3.0 github.com/prometheus/client_model v0.1.0 github.com/prometheus/common v0.7.0 @@ -72,7 +71,7 @@ require ( go.uber.org/atomic v1.5.1 // indirect golang.org/x/crypto v0.0.0-20200406173513-056763e48d71 golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect - golang.org/x/net v0.0.0-20190923162816-aa69164e4478 + golang.org/x/net v0.0.0-20200202094626-16171245cfb2 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e golang.org/x/tools v0.0.0-20191213221258-04c2e8eff935 // indirect diff --git a/go.sum b/go.sum index f39a60310b8..e73b553ffae 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU= cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -19,6 +20,9 @@ github.com/apache/arrow/go/arrow v0.0.0-20200403134915-89ce1cadb678/go.mod h1:QN github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/aws/aws-sdk-go v1.25.48 h1:J82DYDGZHOKHdhx6hD24Tm30c2C3GchYGfN0mf9iKUk= github.com/aws/aws-sdk-go v1.25.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.29.20 h1:vAHJhARpdbdeJstTVaugeHgvVj5lBnfz3blbbD24gfo= +github.com/aws/aws-sdk-go v1.29.20/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg= +github.com/aws/aws-sdk-go v1.29.32 h1:o4I8Qc+h9ht8NXvTHeXZH3EmtSUZ/PC0bg9Wawr+aTA= github.com/beevik/etree v1.0.1/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= @@ -90,6 +94,8 @@ github.com/go-macaron/session v0.0.0-20190805070824-1a3cdc6f5659 h1:YXDFNK98PgKe github.com/go-macaron/session v0.0.0-20190805070824-1a3cdc6f5659/go.mod h1:tLd0QEudXocQckwcpCq5pCuTCuYc24I0bRJDuRe9OuQ= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y= @@ -116,12 +122,14 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/flatbuffers v1.11.0 h1:O7CEyB8Cb3/DmtxODGtLHcEvpr81Jm5qLg/hsHnxA2A= github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= @@ -148,6 +156,7 @@ github.com/hashicorp/go-plugin v1.2.2/go.mod h1:F9eH4LrE/ZsRdbwhfjs9k9HoDUwAHnYt github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0= github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= @@ -240,6 +249,8 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -348,6 +359,7 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= @@ -362,12 +374,15 @@ golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= @@ -388,6 +403,7 @@ golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -408,6 +424,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190802220118-1d1727260058/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= @@ -423,8 +440,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= @@ -433,12 +452,14 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= @@ -477,6 +498,7 @@ gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= xorm.io/builder v0.3.6 h1:ha28mQ2M+TFx96Hxo+iq6tQgnkC9IZkM6D8w9sKHHF8= xorm.io/builder v0.3.6/go.mod h1:LEFAPISnRzG+zxaxj2vPicRwz67BdhFreKg8yv8/TgU= diff --git a/package.json b/package.json index 9904a1b5671..a94533be48f 100644 --- a/package.json +++ b/package.json @@ -205,6 +205,8 @@ "@reduxjs/toolkit": "1.3.4", "@torkelo/react-select": "3.0.8", "@types/braintree__sanitize-url": "4.0.0", + "@types/common-tags": "^1.8.0", + "@types/jsurl": "^1.2.28", "@types/md5": "^2.1.33", "@types/react-loadable": "5.5.2", "@types/react-virtualized-auto-sizer": "1.0.0", @@ -220,6 +222,7 @@ "calculate-size": "1.1.1", "classnames": "2.2.6", "clipboard": "2.0.4", + "common-tags": "^1.8.0", "core-js": "3.6.4", "d3": "5.15.0", "d3-scale-chromatic": "1.5.0", @@ -231,6 +234,7 @@ "immutable": "3.8.2", "is-hotkey": "0.1.6", "jquery": "3.4.1", + "jsurl": "^0.1.5", "lodash": "4.17.15", "lru-cache": "^5.1.1", "marked": "0.6.2", diff --git a/packages/grafana-data/src/dataframe/ArrowDataFrame.ts b/packages/grafana-data/src/dataframe/ArrowDataFrame.ts index 311721a936b..cce08dc5ada 100644 --- a/packages/grafana-data/src/dataframe/ArrowDataFrame.ts +++ b/packages/grafana-data/src/dataframe/ArrowDataFrame.ts @@ -1,4 +1,5 @@ import { DataFrame, FieldType, Field, Vector } from '../types'; + import { Table, ArrowType, @@ -162,15 +163,18 @@ export function grafanaDataFrameToArrowTable(data: DataFrame): Table { } export function resultsToDataFrames(rsp: any): DataFrame[] { - const frames: DataFrame[] = []; - for (const res of Object.values(rsp.results)) { - const r = res as any; - if (r.dataframes) { - for (const b of r.dataframes) { - const t = base64StringToArrowTable(b as string); - frames.push(arrowTableToDataFrame(t)); - } - } + if (rsp === undefined || rsp.results === undefined) { + return []; } + + const results = rsp.results as Array<{ dataframes: string[] }>; + const frames: DataFrame[] = Object.values(results).flatMap(res => { + if (!res.dataframes) { + return []; + } + + return res.dataframes.map((b: string) => arrowTableToDataFrame(base64StringToArrowTable(b))); + }); + return frames; } diff --git a/packages/grafana-data/src/dataframe/FieldCache.ts b/packages/grafana-data/src/dataframe/FieldCache.ts index e7dc574d70d..d3e1c759c15 100644 --- a/packages/grafana-data/src/dataframe/FieldCache.ts +++ b/packages/grafana-data/src/dataframe/FieldCache.ts @@ -58,11 +58,9 @@ export class FieldCache { } getFirstFieldOfType(type: FieldType): FieldWithIndex | undefined { - const arr = this.fieldByType[type]; - if (arr && arr.length > 0) { - return arr[0]; - } - return undefined; + const fields = this.fieldByType[type]; + const firstField = fields.find(field => !(field.config.custom && field.config.custom['Hidden'])); + return firstField; } hasFieldNamed(name: string): boolean { diff --git a/packages/grafana-data/src/dataframe/processDataFrame.ts b/packages/grafana-data/src/dataframe/processDataFrame.ts index c74a40cd04f..02f482c7cc9 100644 --- a/packages/grafana-data/src/dataframe/processDataFrame.ts +++ b/packages/grafana-data/src/dataframe/processDataFrame.ts @@ -178,6 +178,10 @@ export function guessFieldTypeFromNameAndValue(name: string, v: any): FieldType * TODO: better Date/Time support! Look for standard date strings? */ export function guessFieldTypeFromValue(v: any): FieldType { + if (v instanceof Date || isDateTime(v)) { + return FieldType.time; + } + if (isNumber(v)) { return FieldType.number; } @@ -198,10 +202,6 @@ export function guessFieldTypeFromValue(v: any): FieldType { return FieldType.boolean; } - if (v instanceof Date || isDateTime(v)) { - return FieldType.time; - } - return FieldType.other; } @@ -230,17 +230,19 @@ export function guessFieldTypeForField(field: Field): FieldType | undefined { } /** - * @returns a copy of the series with the best guess for each field type - * If the series already has field types defined, they will be used + * @returns A copy of the series with the best guess for each field type. + * If the series already has field types defined, they will be used, unless `guessDefined` is true. + * @param series The DataFrame whose field's types should be guessed + * @param guessDefined Whether to guess types of fields with already defined types */ -export const guessFieldTypes = (series: DataFrame): DataFrame => { - for (let i = 0; i < series.fields.length; i++) { - if (!series.fields[i].type) { +export const guessFieldTypes = (series: DataFrame, guessDefined = false): DataFrame => { + for (const field of series.fields) { + if (!field.type || field.type === FieldType.other || guessDefined) { // Something is missing a type, return a modified copy return { ...series, fields: series.fields.map(field => { - if (field.type && field.type !== FieldType.other) { + if (field.type && field.type !== FieldType.other && !guessDefined) { return field; } // Calculate a reasonable schema value diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index c1e8219c900..5d61134d88d 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -38,6 +38,7 @@ export interface FeatureToggles { * Available only in Grafana Enterprise */ meta: boolean; + cloudwatchLogs: boolean; newVariables: boolean; } diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 54191d9d23e..8e4391a42b5 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -274,6 +274,8 @@ export abstract class DataSourceApi< getVersion?(optionalOptions?: any): Promise; + showContextToggle?(row?: LogRowModel): boolean; + /** * Can be optionally implemented to allow datasource to be a source of annotations for dashboard. To be visible * in the annotation editor `annotations` capability also needs to be enabled in plugin.json. @@ -307,6 +309,8 @@ export interface QueryEditorProps< * Contains query response filtered by refId of QueryResultBase and possible query error */ data?: PanelData; + exploreMode?: ExploreMode; + exploreId?: any; } export enum DataSourceStatus { @@ -329,6 +333,7 @@ export interface ExploreQueryFieldProps< onBlur?: () => void; absoluteRange?: AbsoluteTimeRange; exploreMode?: ExploreMode; + exploreId?: any; } export interface ExploreStartPageProps { diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 1e4bb6582f3..9dc31e6b6e6 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -52,6 +52,7 @@ export class GrafanaBootConfig implements GrafanaConfig { expressions: false, newEdit: false, meta: false, + cloudwatchLogs: false, newVariables: true, }; licenseInfo: LicenseInfo = {} as LicenseInfo; diff --git a/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx b/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx index 19cfdf6e615..f23587424bb 100644 --- a/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx +++ b/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx @@ -10,7 +10,7 @@ interface Props { isFocused?: boolean; isInvalid?: boolean; tooltip?: PopoverContent; - width?: number; + width?: number | 'auto'; } export const FormLabel: FunctionComponent = ({ @@ -23,7 +23,7 @@ export const FormLabel: FunctionComponent = ({ width, ...rest }) => { - const classes = classNames(`gf-form-label width-${width ? width : '10'}`, className, { + const classes = classNames(className, `gf-form-label width-${width ? width : '10'}`, { 'gf-form-label--is-focused': isFocused, 'gf-form-label--is-invalid': isInvalid, }); diff --git a/packages/grafana-ui/src/components/Logs/LogRow.tsx b/packages/grafana-ui/src/components/Logs/LogRow.tsx index c8958a5f948..52f10509450 100644 --- a/packages/grafana-ui/src/components/Logs/LogRow.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRow.tsx @@ -8,6 +8,7 @@ import { LogRowContextQueryErrors, HasMoreContextRows, LogRowContextProvider, + RowContextOptions, } from './LogRowContextProvider'; import { Themeable } from '../../types/theme'; import { withTheme } from '../../themes/index'; @@ -33,8 +34,9 @@ interface Props extends Themeable { onClickFilterLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void; onContextClick?: () => void; - getRowContext: (row: LogRowModel, options?: any) => Promise; + getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise; getFieldLinks?: (field: Field, rowIndex: number) => Array>; + showContextToggle?: (row?: LogRowModel) => boolean; } interface State { @@ -122,6 +124,7 @@ class UnThemedLogRow extends PureComponent { row, showDuplicates, timeZone, + showContextToggle, showLabels, showTime, wrapLogMessage, @@ -176,7 +179,8 @@ class UnThemedLogRow extends PureComponent { hasMoreContextRows={hasMoreContextRows} updateLimit={updateLimit} context={context} - showContext={showContext} + contextIsOpen={showContext} + showContextToggle={showContextToggle} wrapLogMessage={wrapLogMessage} onToggleContext={this.toggleContext} /> diff --git a/packages/grafana-ui/src/components/Logs/LogRowContextProvider.tsx b/packages/grafana-ui/src/components/Logs/LogRowContextProvider.tsx index 8e332b2059a..9acb05be487 100644 --- a/packages/grafana-ui/src/components/Logs/LogRowContextProvider.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRowContextProvider.tsx @@ -5,6 +5,11 @@ import useAsync from 'react-use/lib/useAsync'; import { DataQueryResponse, DataQueryError } from '@grafana/data'; +export interface RowContextOptions { + direction?: 'BACKWARD' | 'FORWARD'; + limit?: number; +} + export interface LogRowContextRows { before?: string[]; after?: string[]; @@ -26,7 +31,7 @@ interface ResultType { interface LogRowContextProviderProps { row: LogRowModel; - getRowContext: (row: LogRowModel, options?: any) => Promise; + getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise; children: (props: { result: LogRowContextRows; errors: LogRowContextQueryErrors; @@ -36,7 +41,7 @@ interface LogRowContextProviderProps { } export const getRowContexts = async ( - getRowContext: (row: LogRowModel, options?: any) => Promise, + getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise, row: LogRowModel, limit: number ) => { diff --git a/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx b/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx index 66e20bdd689..aa68cdf0292 100644 --- a/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx @@ -20,10 +20,11 @@ import { LogMessageAnsi } from './LogMessageAnsi'; interface Props extends Themeable { row: LogRowModel; hasMoreContextRows?: HasMoreContextRows; - showContext: boolean; + contextIsOpen: boolean; wrapLogMessage: boolean; errors?: LogRowContextQueryErrors; context?: LogRowContextRows; + showContextToggle?: (row?: LogRowModel) => boolean; highlighterExpressions?: string[]; getRows: () => LogRowModel[]; onToggleContext: () => void; @@ -74,7 +75,8 @@ class UnThemedLogRowMessage extends PureComponent { hasMoreContextRows, updateLimit, context, - showContext, + contextIsOpen, + showContextToggle, wrapLogMessage, onToggleContext, } = this.props; @@ -97,7 +99,7 @@ class UnThemedLogRowMessage extends PureComponent { return (
- {showContext && context && ( + {contextIsOpen && context && ( { }} /> )} - + {needsHighlighter ? ( { entry )} - {row.searchWords && row.searchWords.length > 0 && ( + {showContextToggle?.(row) && ( - {showContext ? 'Hide' : 'Show'} context + {contextIsOpen ? 'Hide' : 'Show'} context )}
diff --git a/packages/grafana-ui/src/components/Logs/LogRows.tsx b/packages/grafana-ui/src/components/Logs/LogRows.tsx index f01116b073b..991b08f1acd 100644 --- a/packages/grafana-ui/src/components/Logs/LogRows.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRows.tsx @@ -8,6 +8,7 @@ import { getLogRowStyles } from './getLogRowStyles'; //Components import { LogRow } from './LogRow'; +import { RowContextOptions } from './LogRowContextProvider'; export const PREVIEW_LIMIT = 100; export const RENDER_LIMIT = 500; @@ -17,6 +18,7 @@ export interface Props extends Themeable { deduplicatedRows?: LogRowModel[]; dedupStrategy: LogsDedupStrategy; highlighterExpressions?: string[]; + showContextToggle?: (row?: LogRowModel) => boolean; showLabels: boolean; showTime: boolean; wrapLogMessage: boolean; @@ -26,7 +28,7 @@ export interface Props extends Themeable { previewLimit?: number; onClickFilterLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void; - getRowContext?: (row: LogRowModel, options?: any) => Promise; + getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise; getFieldLinks?: (field: Field, rowIndex: number) => Array>; } @@ -72,6 +74,7 @@ class UnThemedLogRows extends PureComponent { render() { const { dedupStrategy, + showContextToggle, showLabels, showTime, wrapLogMessage, @@ -119,6 +122,7 @@ class UnThemedLogRows extends PureComponent { getRowContext={getRowContext} highlighterExpressions={highlighterExpressions} row={row} + showContextToggle={showContextToggle} showDuplicates={showDuplicates} showLabels={showLabels} showTime={showTime} @@ -138,6 +142,7 @@ class UnThemedLogRows extends PureComponent { getRows={getRows} getRowContext={getRowContext} row={row} + showContextToggle={showContextToggle} showDuplicates={showDuplicates} showLabels={showLabels} showTime={showTime} diff --git a/packages/grafana-ui/src/components/QueryField/QueryField.tsx b/packages/grafana-ui/src/components/QueryField/QueryField.tsx index 0abacd390fe..4bea9333fe2 100644 --- a/packages/grafana-ui/src/components/QueryField/QueryField.tsx +++ b/packages/grafana-ui/src/components/QueryField/QueryField.tsx @@ -29,6 +29,7 @@ export interface QueryFieldProps { onRunQuery?: () => void; onBlur?: () => void; onChange?: (value: string) => void; + onClick?: (event: Event, editor: CoreEditor, next: () => any) => any; onTypeahead?: (typeahead: TypeaheadInput) => Promise; onWillApplySuggestion?: (suggestion: string, state: SuggestionsState) => string; placeholder?: string; @@ -167,6 +168,7 @@ export class QueryField extends React.PureComponent { const { onBlur } = this.props; + if (onBlur) { onBlur(); } else { @@ -196,6 +198,7 @@ export class QueryField extends React.PureComponent { this.onChange(change.value, false); diff --git a/packages/grafana-ui/src/components/Select/SelectBase.tsx b/packages/grafana-ui/src/components/Select/SelectBase.tsx index 20974c59be5..49e665f4e28 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.tsx @@ -91,6 +91,7 @@ export function SelectBase({ allowCustomValue = false, autoFocus = false, backspaceRemovesValue = true, + cacheOptions, className, closeMenuOnSelect = true, components, @@ -106,6 +107,7 @@ export function SelectBase({ isLoading = false, isMulti = false, isOpen, + isOptionDisabled, isSearchable = true, loadOptions, loadingMessage = 'Loading options...', @@ -183,6 +185,7 @@ export function SelectBase({ isDisabled: disabled, isLoading, isMulti, + isOptionDisabled, isSearchable, maxMenuHeight, maxVisibleValues, @@ -217,6 +220,7 @@ export function SelectBase({ ReactSelectComponent = allowCustomValue ? AsyncCreatable : ReactAsyncSelect; asyncSelectProps = { loadOptions, + cacheOptions, defaultOptions, }; } @@ -337,6 +341,10 @@ export function SelectBase({ position: 'relative', width: width ? `${8 * width}px` : '100%', }), + option: (provided: any, state: any) => ({ + ...provided, + opacity: state.isDisabled ? 0.5 : 1, + }), }} className={className} {...commonSelectProps} diff --git a/packages/grafana-ui/src/components/Select/types.ts b/packages/grafana-ui/src/components/Select/types.ts index 038a2a169ba..2acace47c8f 100644 --- a/packages/grafana-ui/src/components/Select/types.ts +++ b/packages/grafana-ui/src/components/Select/types.ts @@ -19,6 +19,7 @@ export interface SelectCommonProps { getOptionLabel?: (item: SelectableValue) => string; getOptionValue?: (item: SelectableValue) => string; inputValue?: string; + invalid?: boolean; isClearable?: boolean; isLoading?: boolean; isMulti?: boolean; @@ -51,6 +52,7 @@ export interface SelectCommonProps { value?: SelectValue; /** Sets the width to a multiple of 8px. Should only be used with inline forms. Setting width of the container is preferred in other cases.*/ width?: number; + isOptionDisabled?: () => boolean; } export interface SelectAsyncProps { @@ -58,6 +60,8 @@ export interface SelectAsyncProps { defaultOptions?: boolean | Array>; /** Asynchronously load select options */ loadOptions?: (query: string) => Promise>>; + /** If cacheOptions is true, then the loaded data will be cached. The cache will remain until cacheOptions changes value. */ + cacheOptions?: boolean; /** Message to display when options are loading */ loadingMessage?: string; } diff --git a/packages/grafana-ui/src/components/Typeahead/TypeaheadInfo.tsx b/packages/grafana-ui/src/components/Typeahead/TypeaheadInfo.tsx index a2b6080c831..059bf2fbc34 100644 --- a/packages/grafana-ui/src/components/Typeahead/TypeaheadInfo.tsx +++ b/packages/grafana-ui/src/components/Typeahead/TypeaheadInfo.tsx @@ -8,6 +8,7 @@ const getStyles = (theme: GrafanaTheme, height: number, visible: boolean) => { return { typeaheadItem: css` label: type-ahead-item; + z-index: 11; padding: ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.md}; border-radius: ${theme.border.radius.md}; border: ${selectThemeVariant( diff --git a/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx b/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx index 0d4d1c65dba..3afec001c76 100644 --- a/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx +++ b/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx @@ -28,7 +28,7 @@ const getStyles = (theme: GrafanaTheme) => ({ font-size: ${theme.typography.size.sm}; text-overflow: ellipsis; overflow: hidden; - z-index: 1; + z-index: 11; display: block; white-space: nowrap; cursor: pointer; diff --git a/packages/grafana-ui/src/slate-plugins/index.ts b/packages/grafana-ui/src/slate-plugins/index.ts index 8e7ea693ce4..18da878d2c4 100644 --- a/packages/grafana-ui/src/slate-plugins/index.ts +++ b/packages/grafana-ui/src/slate-plugins/index.ts @@ -5,5 +5,5 @@ export { IndentationPlugin } from './indentation'; export { NewlinePlugin } from './newline'; export { RunnerPlugin } from './runner'; export { SelectionShortcutsPlugin } from './selection_shortcuts'; -export { SlatePrism } from './slate-prism'; +export { SlatePrism, Token } from './slate-prism'; export { SuggestionsPlugin } from './suggestions'; diff --git a/packages/grafana-ui/src/slate-plugins/slate-prism/index.ts b/packages/grafana-ui/src/slate-plugins/slate-prism/index.ts index a83e5daed4b..a5258aefefc 100644 --- a/packages/grafana-ui/src/slate-plugins/slate-prism/index.ts +++ b/packages/grafana-ui/src/slate-plugins/slate-prism/index.ts @@ -4,6 +4,18 @@ import { Plugin } from '@grafana/slate-react'; import Options, { OptionsFormat } from './options'; import TOKEN_MARK from './TOKEN_MARK'; +export interface Token { + content: string; + offsets?: { + start: number; + end: number; + }; + types: string[]; + aliases: string[]; + prev?: Token | null; + next?: Token | null; +} + /** * A Slate plugin to highlight code syntax. */ @@ -15,7 +27,25 @@ export function SlatePrism(optsParam: OptionsFormat = {}): Plugin { if (!opts.onlyIn(node)) { return next(); } - return decorateNode(opts, Block.create(node as Block)); + + const block = Block.create(node as Block); + const grammarName = opts.getSyntax(block); + const grammar = Prism.languages[grammarName]; + + if (!grammar) { + // Grammar not loaded + return []; + } + + // Tokenize the whole block text + const texts = block.getTexts(); + const blockText = texts.map(text => text && text.getText()).join('\n'); + const tokens = Prism.tokenize(blockText, grammar); + const flattened = flattenTokens(tokens); + + // @ts-ignore + editor.setData({ tokens: flattened }); + return decorateNode(opts, tokens, block); }, renderDecoration: (props, editor, next) => @@ -33,18 +63,8 @@ export function SlatePrism(optsParam: OptionsFormat = {}): Plugin { /** * Returns the decoration for a node */ -function decorateNode(opts: Options, block: Block) { - const grammarName = opts.getSyntax(block); - const grammar = Prism.languages[grammarName]; - if (!grammar) { - // Grammar not loaded - return []; - } - - // Tokenize the whole block text +function decorateNode(opts: Options, tokens: Array, block: Block) { const texts = block.getTexts(); - const blockText = texts.map(text => text && text.getText()).join('\n'); - const tokens = Prism.tokenize(blockText, grammar); // The list of decorations to return const decorations: Decoration[] = []; @@ -67,13 +87,17 @@ function decorateNode(opts: Options, block: Block) { className: `prism-token token ${accu}`, block, }); + if (decoration) { decorations.push(decoration); } } offset += token.length; } else { - accu = `${accu} ${token.type} ${token.alias || ''}`; + accu = `${accu} ${token.type}`; + if (token.alias) { + accu += ' ' + token.alias; + } if (typeof token.content === 'string') { const decoration = createDecoration({ @@ -85,6 +109,7 @@ function decorateNode(opts: Options, block: Block) { className: `prism-token token ${accu}`, block, }); + if (decoration) { decorations.push(decoration); } @@ -158,3 +183,71 @@ function createDecoration({ return myDec; } + +function flattenToken(token: string | Prism.Token | Array): Token[] { + if (typeof token === 'string') { + return [ + { + content: token, + types: [], + aliases: [], + }, + ]; + } else if (Array.isArray(token)) { + return token.flatMap(t => flattenToken(t)); + } else if (token instanceof Prism.Token) { + return flattenToken(token.content).flatMap(t => { + let aliases: string[] = []; + if (typeof token.alias === 'string') { + aliases = [token.alias]; + } else { + aliases = token.alias ?? []; + } + + return { + content: t.content, + types: [token.type, ...t.types], + aliases: [...aliases, ...t.aliases], + }; + }); + } + + return []; +} + +export function flattenTokens(token: string | Prism.Token | Array) { + const tokens = flattenToken(token); + + if (!tokens.length) { + return []; + } + + const firstToken = tokens[0]; + firstToken.prev = null; + firstToken.next = tokens.length >= 2 ? tokens[1] : null; + firstToken.offsets = { + start: 0, + end: firstToken.content.length, + }; + + for (let i = 1; i < tokens.length - 1; i++) { + tokens[i].prev = tokens[i - 1]; + tokens[i].next = tokens[i + 1]; + + tokens[i].offsets = { + start: tokens[i - 1].offsets!.end, + end: tokens[i - 1].offsets!.end + tokens[i].content.length, + }; + } + + const lastToken = tokens[tokens.length - 1]; + lastToken.prev = tokens.length >= 2 ? tokens[tokens.length - 2] : null; + lastToken.next = null; + lastToken.offsets = { + start: tokens.length >= 2 ? tokens[tokens.length - 2].offsets!.end : 0, + end: + tokens.length >= 2 ? tokens[tokens.length - 2].offsets!.end + lastToken.content.length : lastToken.content.length, + }; + + return tokens; +} diff --git a/packages/grafana-ui/src/slate-plugins/suggestions.tsx b/packages/grafana-ui/src/slate-plugins/suggestions.tsx index 1a0fe11245f..2d74ab061b9 100644 --- a/packages/grafana-ui/src/slate-plugins/suggestions.tsx +++ b/packages/grafana-ui/src/slate-plugins/suggestions.tsx @@ -217,14 +217,16 @@ const handleTypeahead = async ( // Get decorations associated with the current line const parentBlock = value.document.getClosestBlock(value.focusBlock.key); - const myOffset = value.selection.start.offset - 1; + const selectionStartOffset = value.selection.start.offset - 1; const decorations = parentBlock && parentBlock.getDecorations(editor as any); const filteredDecorations = decorations ? decorations .filter( decoration => - decoration!.start.offset <= myOffset && decoration!.end.offset > myOffset && decoration!.type === TOKEN_MARK + decoration!.start.offset <= selectionStartOffset && + decoration!.end.offset > selectionStartOffset && + decoration!.type === TOKEN_MARK ) .toArray() : []; @@ -235,7 +237,7 @@ const handleTypeahead = async ( decorations .filter( decoration => - decoration!.end.offset <= myOffset && + decoration!.end.offset <= selectionStartOffset && decoration!.type === TOKEN_MARK && decoration!.data.get('className').includes('label-key') ) @@ -272,6 +274,7 @@ const handleTypeahead = async ( value, wrapperClasses, labelKey: labelKey || undefined, + editor, }); const filteredSuggestions = suggestions @@ -280,28 +283,29 @@ const handleTypeahead = async ( return group; } + let newGroup = { ...group }; if (prefix) { // Filter groups based on prefix if (!group.skipFilter) { - group.items = group.items.filter(c => (c.filterText || c.label).length >= prefix.length); + newGroup.items = newGroup.items.filter(c => (c.filterText || c.label).length >= prefix.length); if (group.prefixMatch) { - group.items = group.items.filter(c => (c.filterText || c.label).startsWith(prefix)); + newGroup.items = newGroup.items.filter(c => (c.filterText || c.label).startsWith(prefix)); } else { - group.items = group.items.filter(c => (c.filterText || c.label).includes(prefix)); + newGroup.items = newGroup.items.filter(c => (c.filterText || c.label).includes(prefix)); } } // Filter out the already typed value (prefix) unless it inserts custom text - group.items = group.items.filter(c => c.insertText || (c.filterText || c.label) !== prefix); + newGroup.items = newGroup.items.filter(c => c.insertText || (c.filterText || c.label) !== prefix); } if (!group.skipSort) { - group.items = sortBy(group.items, (item: CompletionItem) => item.sortText || item.label); + newGroup.items = sortBy(newGroup.items, (item: CompletionItem) => item.sortText || item.label); } - return group; + return newGroup; }) - .filter(group => group.items && group.items.length); // Filter out empty groups + .filter(gr => gr.items && gr.items.length); // Filter out empty groups onStateChange({ groupedItems: filteredSuggestions, diff --git a/packages/grafana-ui/src/types/completion.ts b/packages/grafana-ui/src/types/completion.ts index e93162c511e..b56c360faa5 100644 --- a/packages/grafana-ui/src/types/completion.ts +++ b/packages/grafana-ui/src/types/completion.ts @@ -1,5 +1,4 @@ -import { Value } from 'slate'; -import { Editor } from '@grafana/slate-react'; +import { Value, Editor as CoreEditor } from 'slate'; export interface CompletionItemGroup { /** @@ -98,7 +97,7 @@ export interface TypeaheadInput { wrapperClasses: string[]; labelKey?: string; value?: Value; - editor?: Editor; + editor?: CoreEditor; } export interface SuggestionsState { diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index e8288e732f6..0e813c73958 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -52,7 +52,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio return nil, err } - emptySerieCount := 0 + emptySeriesCount := 0 evalMatchCount := 0 var matches []*alerting.EvalMatch @@ -61,7 +61,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio evalMatch := c.Evaluator.Eval(reducedValue) if !reducedValue.Valid { - emptySerieCount++ + emptySeriesCount++ } if context.IsTestRun { @@ -100,7 +100,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio return &alerting.ConditionResult{ Firing: evalMatchCount > 0, - NoDataFound: emptySerieCount == len(seriesList), + NoDataFound: emptySeriesCount == len(seriesList), Operator: c.Operator, EvalMatches: matches, }, nil @@ -224,6 +224,9 @@ func (c *QueryCondition) getRequestForAlertRule(datasource *models.DataSource, t DataSource: datasource, }, }, + Headers: map[string]string{ + "FromAlert": "true", + }, Debug: debug, } diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 54b36a73357..e86c41ccd84 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -2,10 +2,17 @@ package cloudwatch import ( "context" + "fmt" "regexp" + "strconv" + "sync" + "time" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" "github.com/aws/aws-sdk-go/service/ec2/ec2iface" "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb" @@ -15,6 +22,9 @@ type CloudWatchExecutor struct { *models.DataSource ec2Svc ec2iface.EC2API rgtaSvc resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI + + logsClientsByRegion map[string](*cloudwatchlogs.CloudWatchLogs) + mux sync.Mutex } type DatasourceInfo struct { @@ -28,8 +38,43 @@ type DatasourceInfo struct { SecretKey string } -func NewCloudWatchExecutor(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { - return &CloudWatchExecutor{}, nil +const CLOUDWATCH_TS_FORMAT = "2006-01-02 15:04:05.000" + +func (e *CloudWatchExecutor) getLogsClient(region string) (*cloudwatchlogs.CloudWatchLogs, error) { + e.mux.Lock() + defer e.mux.Unlock() + + if logsClient, ok := e.logsClientsByRegion[region]; ok { + return logsClient, nil + } + + dsInfo := retrieveDsInfo(e.DataSource, region) + newLogsClient, err := retrieveLogsClient(dsInfo) + + if err != nil { + return nil, err + } + + e.logsClientsByRegion[region] = newLogsClient + + return newLogsClient, nil +} + +func NewCloudWatchExecutor(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { + dsInfo := retrieveDsInfo(datasource, "default") + defaultLogsClient, err := retrieveLogsClient(dsInfo) + + if err != nil { + return nil, err + } + + logsClientsByRegion := make(map[string](*cloudwatchlogs.CloudWatchLogs)) + logsClientsByRegion[dsInfo.Region] = defaultLogsClient + logsClientsByRegion["default"] = defaultLogsClient + + return &CloudWatchExecutor{ + logsClientsByRegion: logsClientsByRegion, + }, nil } var ( @@ -43,10 +88,60 @@ func init() { aliasFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`) } +func (e *CloudWatchExecutor) alertQuery(ctx context.Context, logsClient *cloudwatchlogs.CloudWatchLogs, queryContext *tsdb.TsdbQuery) (*cloudwatchlogs.GetQueryResultsOutput, error) { + const maxAttempts = 8 + const pollPeriod = 1000 * time.Millisecond + + queryParams := queryContext.Queries[0].Model + startQueryOutput, err := e.executeStartQuery(ctx, logsClient, queryParams, queryContext.TimeRange) + + if err != nil { + return nil, err + } + + requestParams := simplejson.NewFromAny(map[string]interface{}{ + "region": queryParams.Get("region").MustString(""), + "queryId": *startQueryOutput.QueryId, + }) + + ticker := time.NewTicker(pollPeriod) + defer ticker.Stop() + + attemptCount := 1 + for range ticker.C { + if res, err := e.executeGetQueryResults(ctx, logsClient, requestParams); err != nil { + return nil, err + } else if isTerminated(*res.Status) { + return res, err + } else if attemptCount >= maxAttempts { + return res, fmt.Errorf("fetching of query results exceeded max number of attempts") + } + + attemptCount++ + } + + return nil, nil +} + func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSource, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) { var result *tsdb.Response e.DataSource = dsInfo - queryType := queryContext.Queries[0].Model.Get("type").MustString("") + + /* + Unlike many other data sources, with Cloudwatch Logs query requests don't receive the results as the response to the query, but rather + an ID is first returned. Following this, a client is expected to send requests along with the ID until the status of the query is complete, + receiving (possibly partial) results each time. For queries made via dashboards and Explore, the logic of making these repeated queries is handled on + the frontend, but because alerts are executed on the backend the logic needs to be reimplemented here. + */ + queryParams := queryContext.Queries[0].Model + _, fromAlert := queryContext.Headers["FromAlert"] + isLogAlertQuery := fromAlert && queryParams.Get("mode").MustString("") == "Logs" + + if isLogAlertQuery { + return e.executeLogAlertQuery(ctx, queryContext) + } + + queryType := queryParams.Get("type").MustString("") var err error switch queryType { @@ -54,6 +149,8 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc result, err = e.executeMetricFindQuery(ctx, queryContext) case "annotationQuery": result, err = e.executeAnnotationQuery(ctx, queryContext) + case "logAction": + result, err = e.executeLogActions(ctx, queryContext) case "timeSeriesQuery": fallthrough default: @@ -62,3 +159,108 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc return result, err } + +func (e *CloudWatchExecutor) executeLogAlertQuery(ctx context.Context, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) { + queryParams := queryContext.Queries[0].Model + queryParams.Set("subtype", "StartQuery") + queryParams.Set("queryString", queryParams.Get("expression").MustString("")) + + region := queryParams.Get("region").MustString("default") + if region == "default" { + region = e.DataSource.JsonData.Get("defaultRegion").MustString() + queryParams.Set("region", region) + } + + logsClient, err := e.getLogsClient(region) + if err != nil { + return nil, err + } + + result, err := e.executeStartQuery(ctx, logsClient, queryParams, queryContext.TimeRange) + if err != nil { + return nil, err + } + + queryParams.Set("queryId", *result.QueryId) + + // Get Query Results + getQueryResultsOutput, err := e.alertQuery(ctx, logsClient, queryContext) + if err != nil { + return nil, err + } + + dataframe, err := queryResultsToDataframe(getQueryResultsOutput) + if err != nil { + return nil, err + } + + dataframeEnc, err := dataframe.MarshalArrow() + if err != nil { + return nil, err + } + + response := &tsdb.Response{ + Results: make(map[string]*tsdb.QueryResult), + } + + response.Results["A"] = &tsdb.QueryResult{ + RefId: "A", + Dataframes: [][]byte{dataframeEnc}, + } + + return response, nil +} + +func queryResultsToDataframe(results *cloudwatchlogs.GetQueryResultsOutput) (*data.Frame, error) { + rowCount := len(results.Results) + fieldValues := make(map[string]interface{}) + for i, row := range results.Results { + for _, resultField := range row { + // Strip @ptr field from results as it's not needed + if *resultField.Field == "@ptr" { + continue + } + + if _, exists := fieldValues[*resultField.Field]; !exists { + if _, err := time.Parse(CLOUDWATCH_TS_FORMAT, *resultField.Value); err == nil { + fieldValues[*resultField.Field] = make([]*time.Time, rowCount) + } else if _, err := strconv.ParseFloat(*resultField.Value, 64); err == nil { + fieldValues[*resultField.Field] = make([]*float64, rowCount) + } else { + continue + } + } + + if timeField, ok := fieldValues[*resultField.Field].([]*time.Time); ok { + parsedTime, err := time.Parse(CLOUDWATCH_TS_FORMAT, *resultField.Value) + if err != nil { + return nil, err + } + + timeField[i] = &parsedTime + } else if numericField, ok := fieldValues[*resultField.Field].([]*float64); ok { + parsedFloat, err := strconv.ParseFloat(*resultField.Value, 64) + if err != nil { + return nil, err + } + numericField[i] = &parsedFloat + } + } + } + + newFields := make([]*data.Field, 0) + for fieldName, vals := range fieldValues { + newFields = append(newFields, data.NewField(fieldName, nil, vals)) + + if fieldName == "@timestamp" { + newFields[len(newFields)-1].SetConfig(&data.FieldConfig{Title: "Time"}) + } + } + + frame := data.NewFrame("CloudWatchLogsResponse", newFields...) + return frame, nil +} + +func isTerminated(queryStatus string) bool { + return queryStatus == "Complete" || queryStatus == "Cancelled" || queryStatus == "Failed" || queryStatus == "Timeout" +} diff --git a/pkg/tsdb/cloudwatch/credentials.go b/pkg/tsdb/cloudwatch/credentials.go index 3ce8fb5761c..2ab7780fb81 100644 --- a/pkg/tsdb/cloudwatch/credentials.go +++ b/pkg/tsdb/cloudwatch/credentials.go @@ -16,7 +16,9 @@ import ( "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/cloudwatchlogs" "github.com/aws/aws-sdk-go/service/sts" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) @@ -41,7 +43,7 @@ func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) { } credentialCacheLock.RUnlock() - accessKeyId := "" + accessKeyID := "" secretAccessKey := "" sessionToken := "" var expiration *time.Time = nil @@ -78,7 +80,7 @@ func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) { return nil, err } if resp.Credentials != nil { - accessKeyId = *resp.Credentials.AccessKeyId + accessKeyID = *resp.Credentials.AccessKeyId secretAccessKey = *resp.Credentials.SecretAccessKey sessionToken = *resp.Credentials.SessionToken expiration = resp.Credentials.Expiration @@ -96,7 +98,7 @@ func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) { creds := credentials.NewChainCredentials( []credentials.Provider{ &credentials.StaticProvider{Value: credentials.Value{ - AccessKeyID: accessKeyId, + AccessKeyID: accessKeyID, SecretAccessKey: secretAccessKey, SessionToken: sessionToken, }}, @@ -154,20 +156,24 @@ func ec2RoleProvider(sess *session.Session) credentials.Provider { } func (e *CloudWatchExecutor) getDsInfo(region string) *DatasourceInfo { - defaultRegion := e.DataSource.JsonData.Get("defaultRegion").MustString() + return retrieveDsInfo(e.DataSource, region) +} + +func retrieveDsInfo(datasource *models.DataSource, region string) *DatasourceInfo { + defaultRegion := datasource.JsonData.Get("defaultRegion").MustString() if region == "default" { region = defaultRegion } - authType := e.DataSource.JsonData.Get("authType").MustString() - assumeRoleArn := e.DataSource.JsonData.Get("assumeRoleArn").MustString() - decrypted := e.DataSource.DecryptedValues() + authType := datasource.JsonData.Get("authType").MustString() + assumeRoleArn := datasource.JsonData.Get("assumeRoleArn").MustString() + decrypted := datasource.DecryptedValues() accessKey := decrypted["accessKey"] secretKey := decrypted["secretKey"] datasourceInfo := &DatasourceInfo{ Region: region, - Profile: e.DataSource.Database, + Profile: datasource.Database, AuthType: authType, AssumeRoleArn: assumeRoleArn, AccessKey: accessKey, @@ -177,7 +183,7 @@ func (e *CloudWatchExecutor) getDsInfo(region string) *DatasourceInfo { return datasourceInfo } -func (e *CloudWatchExecutor) getAwsConfig(dsInfo *DatasourceInfo) (*aws.Config, error) { +func getAwsConfig(dsInfo *DatasourceInfo) (*aws.Config, error) { creds, err := GetCredentials(dsInfo) if err != nil { return nil, err @@ -193,7 +199,7 @@ func (e *CloudWatchExecutor) getAwsConfig(dsInfo *DatasourceInfo) (*aws.Config, func (e *CloudWatchExecutor) getClient(region string) (*cloudwatch.CloudWatch, error) { datasourceInfo := e.getDsInfo(region) - cfg, err := e.getAwsConfig(datasourceInfo) + cfg, err := getAwsConfig(datasourceInfo) if err != nil { return nil, err } @@ -211,3 +217,23 @@ func (e *CloudWatchExecutor) getClient(region string) (*cloudwatch.CloudWatch, e return client, nil } + +func retrieveLogsClient(datasourceInfo *DatasourceInfo) (*cloudwatchlogs.CloudWatchLogs, error) { + cfg, err := getAwsConfig(datasourceInfo) + if err != nil { + return nil, err + } + + sess, err := session.NewSession(cfg) + if err != nil { + return nil, err + } + + client := cloudwatchlogs.New(sess, cfg) + + client.Handlers.Send.PushFront(func(r *request.Request) { + r.HTTPRequest.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion)) + }) + + return client, nil +} diff --git a/pkg/tsdb/cloudwatch/log_actions.go b/pkg/tsdb/cloudwatch/log_actions.go new file mode 100644 index 00000000000..a2fc95fc3d1 --- /dev/null +++ b/pkg/tsdb/cloudwatch/log_actions.go @@ -0,0 +1,307 @@ +package cloudwatch + +import ( + "context" + "fmt" + "sort" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb" + "github.com/grafana/grafana/pkg/util/errutil" + "golang.org/x/sync/errgroup" +) + +func (e *CloudWatchExecutor) executeLogActions(ctx context.Context, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) { + resultChan := make(chan *tsdb.QueryResult, len(queryContext.Queries)) + eg, ectx := errgroup.WithContext(ctx) + + for _, query := range queryContext.Queries { + query := query + + eg.Go(func() error { + dataframe, err := e.executeLogAction(ectx, queryContext, query) + if err != nil { + return err + } + + dataframeEnc, err := dataframe.MarshalArrow() + if err != nil { + return err + } + + resultChan <- &tsdb.QueryResult{RefId: query.RefId, Dataframes: [][]byte{dataframeEnc}} + return nil + }) + } + + if err := eg.Wait(); err != nil { + return nil, err + } + + close(resultChan) + + response := &tsdb.Response{ + Results: make(map[string]*tsdb.QueryResult), + } + + for result := range resultChan { + response.Results[result.RefId] = result + } + + return response, nil +} + +func (e *CloudWatchExecutor) executeLogAction(ctx context.Context, queryContext *tsdb.TsdbQuery, query *tsdb.Query) (*data.Frame, error) { + parameters := query.Model + subType := query.Model.Get("subtype").MustString() + + defaultRegion := e.DataSource.JsonData.Get("defaultRegion").MustString() + region := parameters.Get("region").MustString(defaultRegion) + logsClient, err := e.getLogsClient(region) + if err != nil { + return nil, err + } + + var data *data.Frame = nil + + switch subType { + case "DescribeLogGroups": + data, err = e.handleDescribeLogGroups(ctx, logsClient, parameters) + case "GetLogGroupFields": + data, err = e.handleGetLogGroupFields(ctx, logsClient, parameters, query.RefId) + case "StartQuery": + data, err = e.handleStartQuery(ctx, logsClient, parameters, queryContext.TimeRange, query.RefId) + case "StopQuery": + data, err = e.handleStopQuery(ctx, logsClient, parameters) + case "GetQueryResults": + data, err = e.handleGetQueryResults(ctx, logsClient, parameters, query.RefId) + case "GetLogEvents": + data, err = e.handleGetLogEvents(ctx, logsClient, parameters) + } + + if err != nil { + return nil, err + } + + return data, nil +} + +func (e *CloudWatchExecutor) handleGetLogEvents(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json) (*data.Frame, error) { + queryRequest := &cloudwatchlogs.GetLogEventsInput{ + Limit: aws.Int64(parameters.Get("limit").MustInt64(10)), + StartFromHead: aws.Bool(parameters.Get("startFromHead").MustBool(false)), + } + + logGroupName, err := parameters.Get("logGroupName").String() + if err != nil { + return nil, fmt.Errorf("Error: Parameter 'logGroupName' is required") + } + queryRequest.SetLogGroupName(logGroupName) + + logStreamName, err := parameters.Get("logStreamName").String() + if err != nil { + return nil, fmt.Errorf("Error: Parameter 'logStream' is required") + } + queryRequest.SetLogStreamName(logStreamName) + + if startTime, err := parameters.Get("startTime").Int64(); err == nil { + queryRequest.SetStartTime(startTime) + } + + if endTime, err := parameters.Get("endTime").Int64(); err == nil { + queryRequest.SetEndTime(endTime) + } + + logEvents, err := logsClient.GetLogEventsWithContext(ctx, queryRequest) + if err != nil { + return nil, errutil.Wrap(err.(awserr.Error).Message(), err) + } + + messages := make([]*string, 0) + timestamps := make([]*int64, 0) + + sort.Slice(logEvents.Events, func(i, j int) bool { + return *(logEvents.Events[i].Timestamp) > *(logEvents.Events[j].Timestamp) + }) + + for _, event := range logEvents.Events { + messages = append(messages, event.Message) + timestamps = append(timestamps, event.Timestamp) + } + + timestampField := data.NewField("ts", nil, timestamps) + timestampField.SetConfig(&data.FieldConfig{Title: "Time"}) + + messageField := data.NewField("line", nil, messages) + + return data.NewFrame("logEvents", timestampField, messageField), nil +} + +func (e *CloudWatchExecutor) handleDescribeLogGroups(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json) (*data.Frame, error) { + logGroupNamePrefix := parameters.Get("logGroupNamePrefix").MustString("") + var response *cloudwatchlogs.DescribeLogGroupsOutput = nil + var err error + + if len(logGroupNamePrefix) < 1 { + response, err = logsClient.DescribeLogGroupsWithContext(ctx, &cloudwatchlogs.DescribeLogGroupsInput{ + Limit: aws.Int64(parameters.Get("limit").MustInt64(50)), + }) + } else { + response, err = logsClient.DescribeLogGroupsWithContext(ctx, &cloudwatchlogs.DescribeLogGroupsInput{ + Limit: aws.Int64(parameters.Get("limit").MustInt64(50)), + LogGroupNamePrefix: aws.String(logGroupNamePrefix), + }) + } + + if err != nil || response == nil { + return nil, errutil.Wrap(err.(awserr.Error).Message(), err) + } + + logGroupNames := make([]*string, 0) + for _, logGroup := range response.LogGroups { + logGroupNames = append(logGroupNames, logGroup.LogGroupName) + } + + groupNamesField := data.NewField("logGroupName", nil, logGroupNames) + frame := data.NewFrame("logGroups", groupNamesField) + + return frame, nil +} + +func (e *CloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json, timeRange *tsdb.TimeRange) (*cloudwatchlogs.StartQueryOutput, error) { + startTime, err := timeRange.ParseFrom() + if err != nil { + return nil, err + } + + endTime, err := timeRange.ParseTo() + if err != nil { + return nil, err + } + + if !startTime.Before(endTime) { + return nil, fmt.Errorf("invalid time range: Start time must be before end time") + } + + startQueryInput := &cloudwatchlogs.StartQueryInput{ + StartTime: aws.Int64(startTime.Unix()), + EndTime: aws.Int64(endTime.Unix()), + Limit: aws.Int64(parameters.Get("limit").MustInt64(1000)), + LogGroupNames: aws.StringSlice(parameters.Get("logGroupNames").MustStringArray()), + QueryString: aws.String("fields @timestamp,@log,@logStream|" + parameters.Get("queryString").MustString("")), + } + return logsClient.StartQueryWithContext(ctx, startQueryInput) +} + +func (e *CloudWatchExecutor) handleStartQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json, timeRange *tsdb.TimeRange, refID string) (*data.Frame, error) { + startQueryResponse, err := e.executeStartQuery(ctx, logsClient, parameters, timeRange) + if err != nil { + return nil, errutil.Wrap(err.(awserr.Error).Message(), err) + } + + dataFrame := data.NewFrame(refID, data.NewField("queryId", nil, []string{*startQueryResponse.QueryId})) + dataFrame.RefID = refID + + clientRegion := parameters.Get("region").MustString("default") + + dataFrame.Meta = &data.FrameMeta{ + Custom: map[string]interface{}{ + "Region": clientRegion, + }, + } + + return dataFrame, nil +} + +func (e *CloudWatchExecutor) executeStopQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json) (*cloudwatchlogs.StopQueryOutput, error) { + queryInput := &cloudwatchlogs.StopQueryInput{ + QueryId: aws.String(parameters.Get("queryId").MustString()), + } + + response, err := logsClient.StopQueryWithContext(ctx, queryInput) + if err != nil { + awsErr := err.(awserr.Error) + // 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. + if awsErr.Code() == "InvalidParameterException" { + response = &cloudwatchlogs.StopQueryOutput{Success: aws.Bool(false)} + err = nil + } else { + err = errutil.Wrap(awsErr.Message(), err) + } + } + + return response, err +} + +func (e *CloudWatchExecutor) handleStopQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json) (*data.Frame, error) { + response, err := e.executeStopQuery(ctx, logsClient, parameters) + if err != nil { + return nil, errutil.Wrap(err.(awserr.Error).Message(), err) + } + + dataFrame := data.NewFrame("StopQueryResponse", data.NewField("success", nil, []bool{*response.Success})) + return dataFrame, nil +} + +func (e *CloudWatchExecutor) executeGetQueryResults(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json) (*cloudwatchlogs.GetQueryResultsOutput, error) { + queryInput := &cloudwatchlogs.GetQueryResultsInput{ + QueryId: aws.String(parameters.Get("queryId").MustString()), + } + + return logsClient.GetQueryResultsWithContext(ctx, queryInput) +} + +func (e *CloudWatchExecutor) handleGetQueryResults(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json, refID string) (*data.Frame, error) { + getQueryResultsOutput, err := e.executeGetQueryResults(ctx, logsClient, parameters) + if err != nil { + return nil, errutil.Wrap(err.(awserr.Error).Message(), err) + } + + dataFrame, err := logsResultsToDataframes(getQueryResultsOutput) + + if err != nil { + return nil, err + } + + dataFrame.Name = refID + dataFrame.RefID = refID + + return dataFrame, nil +} + +func (e *CloudWatchExecutor) handleGetLogGroupFields(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI, parameters *simplejson.Json, refID string) (*data.Frame, error) { + queryInput := &cloudwatchlogs.GetLogGroupFieldsInput{ + LogGroupName: aws.String(parameters.Get("logGroupName").MustString()), + Time: aws.Int64(parameters.Get("time").MustInt64()), + } + + getLogGroupFieldsOutput, err := logsClient.GetLogGroupFieldsWithContext(ctx, queryInput) + if err != nil { + return nil, errutil.Wrap(err.(awserr.Error).Message(), err) + } + + fieldNames := make([]*string, 0) + fieldPercentages := make([]*int64, 0) + + for _, logGroupField := range getLogGroupFieldsOutput.LogGroupFields { + fieldNames = append(fieldNames, logGroupField.Name) + fieldPercentages = append(fieldPercentages, logGroupField.Percent) + } + + dataFrame := data.NewFrame( + refID, + data.NewField("name", nil, fieldNames), + data.NewField("percent", nil, fieldPercentages), + ) + + dataFrame.RefID = refID + + return dataFrame, nil +} diff --git a/pkg/tsdb/cloudwatch/log_actions_test.go b/pkg/tsdb/cloudwatch/log_actions_test.go new file mode 100644 index 00000000000..175e2ae4997 --- /dev/null +++ b/pkg/tsdb/cloudwatch/log_actions_test.go @@ -0,0 +1,218 @@ +package cloudwatch + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb" + + "github.com/stretchr/testify/assert" +) + +//*** +// LogActions Tests +//*** + +func TestHandleDescribeLogGroups_WhenLogGroupNamePrefixIsEmpty(t *testing.T) { + executor := &CloudWatchExecutor{} + + logsClient := &FakeLogsClient{ + Config: aws.Config{ + Region: aws.String("default"), + }, + } + + params := simplejson.NewFromAny(map[string]interface{}{ + "limit": 50, + }) + + frame, err := executor.handleDescribeLogGroups(context.Background(), logsClient, params) + + expectedField := data.NewField("logGroupName", nil, []*string{aws.String("group_a"), aws.String("group_b"), aws.String("group_c")}) + expectedFrame := data.NewFrame("logGroups", expectedField) + + assert.Equal(t, nil, err) + assert.Equal(t, expectedFrame, frame) +} + +func TestHandleDescribeLogGroups_WhenLogGroupNamePrefixIsNotEmpty(t *testing.T) { + executor := &CloudWatchExecutor{} + + logsClient := &FakeLogsClient{ + Config: aws.Config{ + Region: aws.String("default"), + }, + } + + params := simplejson.NewFromAny(map[string]interface{}{ + "logGroupNamePrefix": "g", + }) + + frame, err := executor.handleDescribeLogGroups(context.Background(), logsClient, params) + + expectedField := data.NewField("logGroupName", nil, []*string{aws.String("group_a"), aws.String("group_b"), aws.String("group_c")}) + expectedFrame := data.NewFrame("logGroups", expectedField) + assert.Equal(t, nil, err) + assert.Equal(t, expectedFrame, frame) +} + +func TestHandleGetLogGroupFields_WhenLogGroupNamePrefixIsNotEmpty(t *testing.T) { + executor := &CloudWatchExecutor{} + + logsClient := &FakeLogsClient{ + Config: aws.Config{ + Region: aws.String("default"), + }, + } + + params := simplejson.NewFromAny(map[string]interface{}{ + "logGroupName": "group_a", + "limit": 50, + }) + + frame, err := executor.handleGetLogGroupFields(context.Background(), logsClient, params, "A") + + expectedNameField := data.NewField("name", nil, []*string{aws.String("field_a"), aws.String("field_b"), aws.String("field_c")}) + expectedPercentField := data.NewField("percent", nil, []*int64{aws.Int64(100), aws.Int64(30), aws.Int64(55)}) + expectedFrame := data.NewFrame("A", expectedNameField, expectedPercentField) + expectedFrame.RefID = "A" + + assert.Equal(t, nil, err) + assert.Equal(t, expectedFrame, frame) +} + +func TestExecuteStartQuery(t *testing.T) { + executor := &CloudWatchExecutor{} + + logsClient := &FakeLogsClient{ + Config: aws.Config{ + Region: aws.String("default"), + }, + } + + timeRange := &tsdb.TimeRange{ + From: "1584873443000", + To: "1584700643000", + } + + params := simplejson.NewFromAny(map[string]interface{}{ + "region": "default", + "limit": 50, + "queryString": "fields @message", + }) + + response, err := executor.executeStartQuery(context.Background(), logsClient, params, timeRange) + + var expectedResponse *cloudwatchlogs.StartQueryOutput = nil + + assert.Equal(t, expectedResponse, response) + assert.Equal(t, fmt.Errorf("invalid time range: Start time must be before end time"), err) + +} + +func TestHandleStartQuery(t *testing.T) { + executor := &CloudWatchExecutor{} + + logsClient := &FakeLogsClient{ + Config: aws.Config{ + Region: aws.String("default"), + }, + } + + timeRange := &tsdb.TimeRange{ + From: "1584700643000", + To: "1584873443000", + } + + params := simplejson.NewFromAny(map[string]interface{}{ + "region": "default", + "limit": 50, + "queryString": "fields @message", + }) + + frame, err := executor.handleStartQuery(context.Background(), logsClient, params, timeRange, "A") + + expectedField := data.NewField("queryId", nil, []string{"abcd-efgh-ijkl-mnop"}) + expectedFrame := data.NewFrame("A", expectedField) + expectedFrame.RefID = "A" + expectedFrame.Meta = &data.FrameMeta{ + Custom: map[string]interface{}{ + "Region": "default", + }, + } + + assert.Equal(t, nil, err) + assert.Equal(t, expectedFrame, frame) +} + +func TestHandleStopQuery(t *testing.T) { + executor := &CloudWatchExecutor{} + + logsClient := &FakeLogsClient{ + Config: aws.Config{ + Region: aws.String("default"), + }, + } + + params := simplejson.NewFromAny(map[string]interface{}{ + "queryId": "abcd-efgh-ijkl-mnop", + }) + + frame, err := executor.handleStopQuery(context.Background(), logsClient, params) + + expectedField := data.NewField("success", nil, []bool{true}) + expectedFrame := data.NewFrame("StopQueryResponse", expectedField) + + assert.Equal(t, nil, err) + assert.Equal(t, expectedFrame, frame) +} + +func TestHandleGetQueryResults(t *testing.T) { + executor := &CloudWatchExecutor{} + + logsClient := &FakeLogsClient{ + Config: aws.Config{ + Region: aws.String("default"), + }, + } + + params := simplejson.NewFromAny(map[string]interface{}{ + "queryId": "abcd-efgh-ijkl-mnop", + }) + + frame, err := executor.handleGetQueryResults(context.Background(), logsClient, params, "A") + timeA, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-20 10:37:23.000") + timeB, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-20 10:40:43.000") + expectedTimeField := data.NewField("@timestamp", nil, []*time.Time{ + aws.Time(timeA), aws.Time(timeB), + }) + expectedTimeField.SetConfig(&data.FieldConfig{Title: "Time"}) + + expectedFieldB := data.NewField("field_b", nil, []*string{ + aws.String("b_1"), aws.String("b_2"), + }) + + expectedFrame := data.NewFrame("A", expectedTimeField, expectedFieldB) + expectedFrame.RefID = "A" + + expectedFrame.Meta = &data.FrameMeta{ + Custom: map[string]interface{}{ + "Status": "Complete", + "Statistics": cloudwatchlogs.QueryStatistics{ + BytesScanned: aws.Float64(512), + RecordsMatched: aws.Float64(256), + RecordsScanned: aws.Float64(1024), + }, + }, + } + + assert.Equal(t, nil, err) + assert.ElementsMatch(t, expectedFrame.Fields, frame.Fields) + assert.Equal(t, expectedFrame.Meta, frame.Meta) +} diff --git a/pkg/tsdb/cloudwatch/log_query.go b/pkg/tsdb/cloudwatch/log_query.go new file mode 100644 index 00000000000..43cc179e9cc --- /dev/null +++ b/pkg/tsdb/cloudwatch/log_query.go @@ -0,0 +1,81 @@ +package cloudwatch + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/grafana/grafana-plugin-sdk-go/data" +) + +func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput) (*data.Frame, error) { + rowCount := len(response.Results) + fieldValues := make(map[string]interface{}) + for i, row := range response.Results { + for _, resultField := range row { + // Strip @ptr field from results as it's not needed + if *resultField.Field == "@ptr" { + continue + } + + if *resultField.Field == "@timestamp" { + if _, exists := fieldValues[*resultField.Field]; !exists { + fieldValues[*resultField.Field] = make([]*time.Time, rowCount) + } + + parsedTime, err := time.Parse(CLOUDWATCH_TS_FORMAT, *resultField.Value) + if err != nil { + return nil, err + } + + fieldValues[*resultField.Field].([]*time.Time)[i] = &parsedTime + } else { + if _, exists := fieldValues[*resultField.Field]; !exists { + // Check if field is time field + if _, err := time.Parse(CLOUDWATCH_TS_FORMAT, *resultField.Value); err == nil { + fieldValues[*resultField.Field] = make([]*time.Time, rowCount) + } else { + fieldValues[*resultField.Field] = make([]*string, rowCount) + } + } + + if timeField, ok := fieldValues[*resultField.Field].([]*time.Time); ok { + parsedTime, err := time.Parse(CLOUDWATCH_TS_FORMAT, *resultField.Value) + if err != nil { + return nil, err + } + + timeField[i] = &parsedTime + } else { + fieldValues[*resultField.Field].([]*string)[i] = resultField.Value + } + } + } + } + + newFields := make([]*data.Field, 0) + for fieldName, vals := range fieldValues { + newFields = append(newFields, data.NewField(fieldName, nil, vals)) + + if fieldName == "@timestamp" { + newFields[len(newFields)-1].SetConfig(&data.FieldConfig{Title: "Time"}) + } else if fieldName == "@logStream" || fieldName == "@log" { + newFields[len(newFields)-1].SetConfig( + &data.FieldConfig{ + Custom: map[string]interface{}{ + "Hidden": true, + }, + }, + ) + } + } + + frame := data.NewFrame("CloudWatchLogsResponse", newFields...) + frame.Meta = &data.FrameMeta{ + Custom: map[string]interface{}{ + "Status": *response.Status, + "Statistics": *response.Statistics, + }, + } + + return frame, nil +} diff --git a/pkg/tsdb/cloudwatch/log_query_test.go b/pkg/tsdb/cloudwatch/log_query_test.go new file mode 100644 index 00000000000..2f457b9c4c7 --- /dev/null +++ b/pkg/tsdb/cloudwatch/log_query_test.go @@ -0,0 +1,161 @@ +package cloudwatch + +import ( + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/grafana/grafana-plugin-sdk-go/data" + + "github.com/stretchr/testify/assert" +) + +//*** +// LogQuery tests +//*** + +func TestLogsResultsToDataframes(t *testing.T) { + fakeCloudwatchResponse := &cloudwatchlogs.GetQueryResultsOutput{ + Results: [][]*cloudwatchlogs.ResultField{ + { + &cloudwatchlogs.ResultField{ + Field: aws.String("@ptr"), + Value: aws.String("fake ptr"), + }, + &cloudwatchlogs.ResultField{ + Field: aws.String("@timestamp"), + Value: aws.String("2020-03-02 15:04:05.000"), + }, + &cloudwatchlogs.ResultField{ + Field: aws.String("line"), + Value: aws.String("test message 1"), + }, + &cloudwatchlogs.ResultField{ + Field: aws.String("@logStream"), + Value: aws.String("fakelogstream"), + }, + &cloudwatchlogs.ResultField{ + Field: aws.String("@log"), + Value: aws.String("fakelog"), + }, + }, + { + &cloudwatchlogs.ResultField{ + Field: aws.String("@ptr"), + Value: aws.String("fake ptr"), + }, + &cloudwatchlogs.ResultField{ + Field: aws.String("@timestamp"), + Value: aws.String("2020-03-02 16:04:05.000"), + }, + &cloudwatchlogs.ResultField{ + Field: aws.String("line"), + Value: aws.String("test message 2"), + }, + &cloudwatchlogs.ResultField{ + Field: aws.String("@logStream"), + Value: aws.String("fakelogstream"), + }, + &cloudwatchlogs.ResultField{ + Field: aws.String("@log"), + Value: aws.String("fakelog"), + }, + }, + { + &cloudwatchlogs.ResultField{ + Field: aws.String("@ptr"), + Value: aws.String("fake ptr"), + }, + &cloudwatchlogs.ResultField{ + Field: aws.String("@timestamp"), + Value: aws.String("2020-03-02 17:04:05.000"), + }, + &cloudwatchlogs.ResultField{ + Field: aws.String("line"), + Value: aws.String("test message 3"), + }, + &cloudwatchlogs.ResultField{ + Field: aws.String("@logStream"), + Value: aws.String("fakelogstream"), + }, + &cloudwatchlogs.ResultField{ + Field: aws.String("@log"), + Value: aws.String("fakelog"), + }, + }, + }, + Status: aws.String("ok"), + Statistics: &cloudwatchlogs.QueryStatistics{ + BytesScanned: aws.Float64(2000), + RecordsMatched: aws.Float64(3), + RecordsScanned: aws.Float64(5000), + }, + } + + dataframes, _ := logsResultsToDataframes(fakeCloudwatchResponse) + timeA, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-02 15:04:05.000") + timeB, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-02 16:04:05.000") + timeC, _ := time.Parse("2006-01-02 15:04:05.000", "2020-03-02 17:04:05.000") + timeVals := []*time.Time{ + &timeA, &timeB, &timeC, + } + timeField := data.NewField("@timestamp", nil, timeVals) + timeField.SetConfig(&data.FieldConfig{Title: "Time"}) + + lineField := data.NewField("line", nil, []*string{ + aws.String("test message 1"), + aws.String("test message 2"), + aws.String("test message 3"), + }) + + logStreamField := data.NewField("@logStream", nil, []*string{ + aws.String("fakelogstream"), + aws.String("fakelogstream"), + aws.String("fakelogstream"), + }) + logStreamField.SetConfig(&data.FieldConfig{ + Custom: map[string]interface{}{ + "Hidden": true, + }, + }) + + logField := data.NewField("@log", nil, []*string{ + aws.String("fakelog"), + aws.String("fakelog"), + aws.String("fakelog"), + }) + logField.SetConfig(&data.FieldConfig{ + Custom: map[string]interface{}{ + "Hidden": true, + }, + }) + + expectedDataframe := &data.Frame{ + Name: "CloudWatchLogsResponse", + Fields: []*data.Field{ + timeField, + lineField, + logStreamField, + logField, + }, + RefID: "", + Meta: &data.FrameMeta{ + Custom: map[string]interface{}{ + "Status": "ok", + "Statistics": cloudwatchlogs.QueryStatistics{ + BytesScanned: aws.Float64(2000), + RecordsMatched: aws.Float64(3), + RecordsScanned: aws.Float64(5000), + }, + }, + }, + } + + // Splitting these assertions up so it's clearer what's wrong should the test + // fail in the future + assert.Equal(t, expectedDataframe.Name, dataframes.Name) + assert.Equal(t, expectedDataframe.RefID, dataframes.RefID) + assert.Equal(t, expectedDataframe.Meta, dataframes.Meta) + assert.ElementsMatch(t, expectedDataframe.Fields, dataframes.Fields) +} diff --git a/pkg/tsdb/cloudwatch/logs_client_mock.go b/pkg/tsdb/cloudwatch/logs_client_mock.go new file mode 100644 index 00000000000..1a4a67f209c --- /dev/null +++ b/pkg/tsdb/cloudwatch/logs_client_mock.go @@ -0,0 +1,106 @@ +package cloudwatch + +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" +) + +type FakeLogsClient struct { + cloudwatchlogsiface.CloudWatchLogsAPI + Config aws.Config +} + +func (f FakeLogsClient) DescribeLogGroupsWithContext(ctx context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, option ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { + return &cloudwatchlogs.DescribeLogGroupsOutput{ + LogGroups: []*cloudwatchlogs.LogGroup{ + { + LogGroupName: aws.String("group_a"), + }, + { + LogGroupName: aws.String("group_b"), + }, + { + LogGroupName: aws.String("group_c"), + }, + }, + }, nil +} + +func (f FakeLogsClient) GetLogGroupFieldsWithContext(ctx context.Context, input *cloudwatchlogs.GetLogGroupFieldsInput, option ...request.Option) (*cloudwatchlogs.GetLogGroupFieldsOutput, error) { + return &cloudwatchlogs.GetLogGroupFieldsOutput{ + LogGroupFields: []*cloudwatchlogs.LogGroupField{ + { + Name: aws.String("field_a"), + Percent: aws.Int64(100), + }, + { + Name: aws.String("field_b"), + Percent: aws.Int64(30), + }, + { + Name: aws.String("field_c"), + Percent: aws.Int64(55), + }, + }, + }, nil +} + +func (f FakeLogsClient) StartQueryWithContext(ctx context.Context, input *cloudwatchlogs.StartQueryInput, option ...request.Option) (*cloudwatchlogs.StartQueryOutput, error) { + return &cloudwatchlogs.StartQueryOutput{ + QueryId: aws.String("abcd-efgh-ijkl-mnop"), + }, nil +} + +func (f FakeLogsClient) StopQueryWithContext(ctx context.Context, input *cloudwatchlogs.StopQueryInput, option ...request.Option) (*cloudwatchlogs.StopQueryOutput, error) { + return &cloudwatchlogs.StopQueryOutput{ + Success: aws.Bool(true), + }, nil +} + +func (f FakeLogsClient) GetQueryResultsWithContext(ctx context.Context, input *cloudwatchlogs.GetQueryResultsInput, option ...request.Option) (*cloudwatchlogs.GetQueryResultsOutput, error) { + return &cloudwatchlogs.GetQueryResultsOutput{ + Results: [][]*cloudwatchlogs.ResultField{ + { + { + Field: aws.String("@timestamp"), + Value: aws.String("2020-03-20 10:37:23.000"), + }, + { + Field: aws.String("field_b"), + Value: aws.String("b_1"), + }, + { + Field: aws.String("@ptr"), + Value: aws.String("abcdefg"), + }, + }, + + { + { + Field: aws.String("@timestamp"), + Value: aws.String("2020-03-20 10:40:43.000"), + }, + { + Field: aws.String("field_b"), + Value: aws.String("b_2"), + }, + { + Field: aws.String("@ptr"), + Value: aws.String("hijklmnop"), + }, + }, + }, + + Statistics: &cloudwatchlogs.QueryStatistics{ + BytesScanned: aws.Float64(512), + RecordsMatched: aws.Float64(256), + RecordsScanned: aws.Float64(1024), + }, + + Status: aws.String("Complete"), + }, nil +} diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index 4484f6db866..cc843bde6db 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -469,7 +469,7 @@ func (e *CloudWatchExecutor) handleGetDimensionValues(ctx context.Context, param func (e *CloudWatchExecutor) ensureClientSession(region string) error { if e.ec2Svc == nil { dsInfo := e.getDsInfo(region) - cfg, err := e.getAwsConfig(dsInfo) + cfg, err := getAwsConfig(dsInfo) if err != nil { return fmt.Errorf("Failed to call ec2:getAwsConfig, %v", err) } @@ -595,7 +595,7 @@ func (e *CloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context, func (e *CloudWatchExecutor) ensureRGTAClientSession(region string) error { if e.rgtaSvc == nil { dsInfo := e.getDsInfo(region) - cfg, err := e.getAwsConfig(dsInfo) + cfg, err := getAwsConfig(dsInfo) if err != nil { return fmt.Errorf("Failed to call ec2:getAwsConfig, %v", err) } diff --git a/pkg/tsdb/cloudwatch/request_parser.go b/pkg/tsdb/cloudwatch/request_parser.go index 6364cdefe08..a8c8178cb1d 100644 --- a/pkg/tsdb/cloudwatch/request_parser.go +++ b/pkg/tsdb/cloudwatch/request_parser.go @@ -14,7 +14,7 @@ import ( "github.com/grafana/grafana/pkg/tsdb" ) -// Parses the json queries and returns a requestQuery. The requstQuery has a 1 to 1 mapping to a query editor row +// Parses the json queries and returns a requestQuery. The requestQuery has a 1 to 1 mapping to a query editor row func (e *CloudWatchExecutor) parseQueries(queryContext *tsdb.TsdbQuery, startTime time.Time, endTime time.Time) (map[string][]*requestQuery, error) { requestQueries := make(map[string][]*requestQuery) diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index 13683534cf3..6d4267b4f64 100644 --- a/pkg/tsdb/models.go +++ b/pkg/tsdb/models.go @@ -10,6 +10,7 @@ import ( type TsdbQuery struct { TimeRange *TimeRange Queries []*Query + Headers map[string]string Debug bool User *models.SignedInUser } diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 5f12338edc8..ddf6ca9a840 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -16,7 +16,6 @@ import { LogsMetaKind, LogsDedupStrategy, GraphSeriesXY, - dateTime, toUtc, NullValueMode, toDataFrame, @@ -26,6 +25,7 @@ import { TimeZone, getDisplayProcessor, textUtil, + dateTime, } from '@grafana/data'; import { getThemeColor } from 'app/core/utils/colors'; @@ -232,17 +232,19 @@ export function dataFrameToLogsModel( }; } -function separateLogsAndMetrics(dataFrame: DataFrame[]) { +function separateLogsAndMetrics(dataFrames: DataFrame[]) { const metricSeries: DataFrame[] = []; const logSeries: DataFrame[] = []; - for (const series of dataFrame) { - if (isLogsData(series)) { - logSeries.push(series); + for (const dataFrame of dataFrames) { + if (isLogsData(dataFrame)) { + logSeries.push(dataFrame); continue; } - metricSeries.push(series); + if (dataFrame.length > 0) { + metricSeries.push(dataFrame); + } } return { logSeries, metricSeries }; @@ -273,9 +275,9 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi const allSeries: LogFields[] = logSeries.map(series => { const fieldCache = new FieldCache(series); - // Assume the first string field in the dataFrame is the message. This was right so far but probably needs some - // more explicit checks. - const stringField = fieldCache.getFirstFieldOfType(FieldType.string); + const stringField = fieldCache.hasFieldNamed('line') + ? fieldCache.getFieldByName('line') + : fieldCache.getFirstFieldOfType(FieldType.string); if (stringField?.labels) { allLabels.push(stringField.labels); } diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 012545d9e2b..503589bd48a 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -239,8 +239,9 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { }; const datasource = parsed[ParseUrlStateIndex.Datasource]; const parsedSegments = parsed.slice(ParseUrlStateIndex.SegmentsStart); - const metricProperties = ['expr', 'target', 'datasource', 'query']; + const metricProperties = ['expr', 'expression', 'target', 'datasource', 'query']; const queries = parsedSegments.filter(segment => isSegment(segment, ...metricProperties)); + const modeObj = parsedSegments.filter(segment => isSegment(segment, 'mode'))[0]; const mode = modeObj ? modeObj.mode : ExploreMode.Metrics; diff --git a/public/app/features/alerting/state/reducers.test.ts b/public/app/features/alerting/state/reducers.test.ts index 63bf3d7c22d..6d65ebdc8eb 100644 --- a/public/app/features/alerting/state/reducers.test.ts +++ b/public/app/features/alerting/state/reducers.test.ts @@ -4,6 +4,11 @@ import { AlertRuleDTO, AlertRulesState } from 'app/types'; import { reducerTester } from '../../../../test/core/redux/reducerTester'; describe('Alert rules', () => { + const realDateNow = Date.now.bind(global.Date); + const anchorUnix = dateTime('2019-09-04T10:01:01+02:00').valueOf(); + const dateNowStub = jest.fn(() => anchorUnix); + global.Date.now = dateNowStub; + const newStateDate = dateTime().subtract(1, 'y'); const newStateDateFormatted = newStateDate.format('YYYY-MM-DD'); const newStateDateAge = newStateDate.fromNow(true); @@ -82,6 +87,10 @@ describe('Alert rules', () => { }, ]; + afterAll(() => { + global.Date.now = realDateNow; + }); + describe('when loadAlertRules is dispatched', () => { it('then state should be correct', () => { reducerTester() diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 44565d708eb..3ab32358aef 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -8,6 +8,7 @@ import memoizeOne from 'memoize-one'; // Services & Utils import store from 'app/core/store'; +import config from 'app/core/config'; // Components import { ErrorBoundaryAlert, stylesFactory, withTheme } from '@grafana/ui'; @@ -310,6 +311,10 @@ export class Explore extends React.PureComponent { const StartPage = datasourceInstance?.components?.ExploreStartPage; const showStartPage = !queryResponse || queryResponse.state === LoadingState.NotStarted; + // TEMP: Remove for 7.0 + const cloudwatchLogsDisabled = + datasourceInstance?.meta?.id === 'cloudwatch' && !config.featureToggles.cloudwatchLogs; + // gets an error without a refID, so non-query-row-related error, like a connection error const queryErrors = queryResponse.error ? [queryResponse.error] : undefined; const queryError = getFirstNonQueryRowSpecificError(queryErrors); @@ -372,7 +377,7 @@ export class Explore extends React.PureComponent { {mode === ExploreMode.Metrics && ( )} - {mode === ExploreMode.Logs && ( + {mode === ExploreMode.Logs && !cloudwatchLogsDisabled && ( { return { @@ -68,6 +77,7 @@ interface StateProps { datasourceLoading?: boolean; containerWidth: number; datasourceName?: string; + datasourceInstance: DataSourceApi; } interface DispatchProps { @@ -178,6 +188,7 @@ export class UnConnectedExploreToolbar extends PureComponent { isPaused, originPanelId, datasourceLoading, + datasourceInstance, containerWidth, } = this.props; @@ -191,6 +202,12 @@ export class UnConnectedExploreToolbar extends PureComponent { const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false; const showSmallTimePicker = splitted || containerWidth < 1210; + // TEMP: Remove for 7.0 + const cloudwatchLogsDisabled = + datasourceInstance?.meta?.id === 'cloudwatch' && !config.featureToggles.cloudwatchLogs; + + const showModeToggle = supportedModes.length > 1 && !cloudwatchLogsDisabled; + return (
@@ -233,7 +250,7 @@ export class UnConnectedExploreToolbar extends PureComponent { hideTextValue={showSmallDataSourcePicker} />
- {supportedModes.length > 1 ? ( + {showModeToggle ? (
boolean; onChangeTime: (range: AbsoluteTimeRange) => void; onClickFilterLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void; @@ -60,7 +62,7 @@ interface Props { onStopScanning?: () => void; onDedupStrategyChange: (dedupStrategy: LogsDedupStrategy) => void; onToggleLogLevel: (hiddenLogLevels: LogLevel[]) => void; - getRowContext?: (row: LogRowModel, options?: any) => Promise; + getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise; getFieldLinks: (field: Field, rowIndex: number) => Array>; } @@ -149,6 +151,7 @@ export class Logs extends PureComponent { timeZone, scanning, scanRange, + showContextToggle, width, dedupedRows, absoluteRange, @@ -240,6 +243,7 @@ export class Logs extends PureComponent { rowLimit={logRows ? logRows.length : undefined} onClickFilterLabel={onClickFilterLabel} onClickFilterOutLabel={onClickFilterOutLabel} + showContextToggle={showContextToggle} showLabels={showLabels} showTime={showTime} wrapLogMessage={wrapLogMessage} diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index 87991c4d950..37bc1b7c6cf 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -153,6 +153,7 @@ export class LogsContainer extends PureComponent { timeZone={timeZone} scanning={scanning} scanRange={range.raw} + showContextToggle={this.props.datasourceInstance?.showContextToggle} width={width} getRowContext={this.getLogRowContext} getFieldLinks={this.getFieldLinks} diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx index a758459f21e..94448b4cae7 100644 --- a/public/app/features/explore/QueryRow.tsx +++ b/public/app/features/explore/QueryRow.tsx @@ -121,6 +121,7 @@ export class QueryRow extends PureComponent { queryResponse, mode, latency, + exploreId, } = this.props; const canToggleEditorModes = @@ -152,6 +153,7 @@ export class QueryRow extends PureComponent { data={queryResponse} absoluteRange={absoluteRange} exploreMode={mode} + exploreId={exploreId} /> ) : ( { splitted: boolean; title: string; - onClick: () => void; + onClick?: () => void; buttonClassName?: string; icon?: IconName; iconClassName?: string; @@ -21,7 +21,7 @@ function formatBtnTitle(title: string, iconSide?: string): string { return iconSide === IconSide.left ? '\xA0' + title : iconSide === IconSide.right ? title + '\xA0' : title; } -export const ResponsiveButton = forwardRef((props, ref) => { +export const ResponsiveButton = forwardRef((props, ref) => { const defaultProps = { iconSide: IconSide.left, }; @@ -40,10 +40,11 @@ export const ResponsiveButton = forwardRef((props, ref) = } = props; return ( -
+