From 21a3f2b13f03d205b6f3686f31a7b789d1584637 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Fri, 1 Aug 2025 18:18:53 +0100 Subject: [PATCH] Move notification historian to grafana/alerting --- go.mod | 4 +- go.sum | 9 +- go.work.sum | 46 ++ .../annotationsimpl/loki/historian_store.go | 5 +- .../loki/historian_store_test.go | 4 +- pkg/services/ngalert/client/client.go | 125 ------ pkg/services/ngalert/client/client_test.go | 29 -- pkg/services/ngalert/lokiclient/client.go | 333 -------------- .../ngalert/lokiclient/client_test.go | 409 ------------------ pkg/services/ngalert/lokiclient/encode.go | 107 ----- pkg/services/ngalert/lokiclient/testing.go | 45 -- pkg/services/ngalert/lokiconfig/lokiconfig.go | 48 ++ .../ngalert/lokiconfig/lokiconfig_test.go | 94 ++++ pkg/services/ngalert/ngalert.go | 10 +- pkg/services/ngalert/notifier/historian.go | 190 -------- .../ngalert/notifier/historian_test.go | 126 ------ .../ngalert/remote/client/alertmanager.go | 2 +- pkg/services/ngalert/remote/client/mimir.go | 2 +- pkg/services/ngalert/state/historian/loki.go | 4 +- .../ngalert/state/historian/loki_test.go | 4 +- 20 files changed, 211 insertions(+), 1385 deletions(-) delete mode 100644 pkg/services/ngalert/client/client.go delete mode 100644 pkg/services/ngalert/client/client_test.go delete mode 100644 pkg/services/ngalert/lokiclient/client.go delete mode 100644 pkg/services/ngalert/lokiclient/client_test.go delete mode 100644 pkg/services/ngalert/lokiclient/encode.go delete mode 100644 pkg/services/ngalert/lokiclient/testing.go create mode 100644 pkg/services/ngalert/lokiconfig/lokiconfig.go create mode 100644 pkg/services/ngalert/lokiconfig/lokiconfig_test.go delete mode 100644 pkg/services/ngalert/notifier/historian.go delete mode 100644 pkg/services/ngalert/notifier/historian_test.go diff --git a/go.mod b/go.mod index f8c2a0a7b16..aa3657614e3 100644 --- a/go.mod +++ b/go.mod @@ -90,7 +90,7 @@ require ( github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics github.com/grafana/dataplane/sdata v0.0.9 // @grafana/observability-metrics - github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 // @grafana/grafana-backend-group + github.com/grafana/dskit v0.0.0-20250723143816-ff33c5829b96 // @grafana/grafana-backend-group github.com/grafana/e2e v0.1.1 // @grafana-app-platform-squad github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f // @grafana/sharing-squad github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d // @grafana/grafana-operator-experience-squad @@ -627,3 +627,5 @@ exclude k8s.io/client-go v12.0.0+incompatible // only used to run tests and not required for building the Grafana binary. // Since the test data doesn't contain a license file we exclude it. exclude github.com/RoaringBitmap/real-roaring-datasets v0.0.0-20190726190000-eb7c87156f76 + +replace github.com/grafana/alerting => ../alerting diff --git a/go.sum b/go.sum index d3ce4af5ca7..13176337c94 100644 --- a/go.sum +++ b/go.sum @@ -1445,9 +1445,8 @@ github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3 h1:6amM4HsNPOvMLVc2ZnyqrjeQ92YAVWn7T4WBKK87inY= github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= -github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= -github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= @@ -1580,8 +1579,6 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/alerting v0.0.0-20250729175202-b4b881b7b263 h1:hcr/AmPB0KL4H+gCEFIdKUnkihTxGAkAOiZA7GDYoL8= -github.com/grafana/alerting v0.0.0-20250729175202-b4b881b7b263/go.mod h1:VKxaR93Gff0ZlO2sPcdPVob1a/UzArFEW5zx3Bpyhls= github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43 h1:vVPT0i5Y1vI6qzecYStV2yk7cHKrC3Pc7AgvwT5KydQ= github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI= github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 h1:NlkGMnVi/oUn6Cr90QbJYpQJ4FnjyAIG9Ex5GtTZIzw= @@ -1590,8 +1587,8 @@ github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmO github.com/grafana/dataplane/examples v0.0.1/go.mod h1:h5YwY8s407/17XF5/dS8XrUtsTVV2RnuW8+m1Mp46mg= github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s= github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= -github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 h1:qcSGhr691f1mmPHwg2svGyO40Ex92G02aOyHzP6XHCE= -github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914/go.mod h1:OiN4P4aC6LwLzLbEupH3Ue83VfQoNMfG48rsna8jI/E= +github.com/grafana/dskit v0.0.0-20250723143816-ff33c5829b96 h1:Wsc21GtcFgqqVuP6SCKaQDoliVW5Agcrmb3PYBUwYS4= +github.com/grafana/dskit v0.0.0-20250723143816-ff33c5829b96/go.mod h1:kImsvJ1xnmeT9Z6StK+RdEKLzlpzBsKwJbEQfmBJdFs= github.com/grafana/e2e v0.1.1 h1:/b6xcv5BtoBnx8cZnCiey9DbjEc8z7gXHO5edoeRYxc= github.com/grafana/e2e v0.1.1/go.mod h1:RpNLgae5VT+BUHvPE+/zSypmOXKwEu4t+tnEMS1ATaE= github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f h1:5xkjl5Y/j2QefJKOtTfyD1wXlVsQ2yEXmd0u82h5obs= diff --git a/go.work.sum b/go.work.sum index d129bd22b1f..12260009815 100644 --- a/go.work.sum +++ b/go.work.sum @@ -553,6 +553,8 @@ github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0 h1:kAtNAWwvTt5+iew6baV0kbO github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0/go.mod h1:VRKXU8C7Y/aUKjRBTGfw0Ndv4YqNxlB8zAPJJDxbASE= github.com/GoogleCloudPlatform/cloudsql-proxy v1.37.6 h1:UucmvNRPE75F3KzT68GHhKzOPwttxiFkh1d5LTTywW8= github.com/GoogleCloudPlatform/cloudsql-proxy v1.37.6/go.mod h1:XGripOBEUAcge8IUWR/NMAB5qO9k82tkbpoewBpyjYQ= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 h1:DBjmt6/otSdULyJdVg2BlG0qGZO5tKL4VzOs0jpvw5Q= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0/go.mod h1:ZV4VOm0/eHR06JLrXWe09068dHpr3TRpY9Uo7T+anuA= @@ -774,10 +776,12 @@ github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892 h1:qg9VbHo1TlL0KDM0vYvBG9EY0X0Yku5WYIPoFWt8f6o= github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892/go.mod h1:CTDl0pzVzE5DEzZhPfvhY/9sPFMQIxaJ9VAMs9AagrE= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g= github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgfR0jcVVXuTsYv/erY2NnEDqwRojbxR1rBYA= +github.com/dgryski/go-ddmin v0.0.0-20210904190556-96a6d69f1034 h1:BuCyszxPxUjBrYW2HNVrimC0rBUs2U27jCJGVh0IKTM= github.com/dgryski/go-ddmin v0.0.0-20210904190556-96a6d69f1034/go.mod h1:zz4KxBkcXUWKjIcrc+uphJ1gPh/t18ymGm3PmQ+VGTk= github.com/dgryski/go-sip13 v0.0.0-20190329191031-25c5027a8c7b h1:Yqiad0+sloMPdd/0Fg22actpFx0dekpzt1xJmVNVkU0= github.com/dhui/dktest v0.3.0 h1:kwX5a7EkLcjo7VpsPQSYJcKGbXBXdjI9FGjuUj1jn6I= @@ -844,6 +848,7 @@ github.com/fluent/fluent-bit-go v0.0.0-20230731091245-a7a013e2473c h1:yKN46XJHYC github.com/fluent/fluent-bit-go v0.0.0-20230731091245-a7a013e2473c/go.mod h1:L92h+dgwElEyUuShEwjbiHjseW410WIcNz+Bjutc8YQ= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsouza/fake-gcs-server v1.7.0 h1:Un0BXUXrRWYSmYyC1Rqm2e2WJfTPyDy/HGMz31emTi8= github.com/fsouza/fake-gcs-server v1.52.2 h1:j6ne83nqHrlX5EEor7WWVIKdBsztGtwJ1J2mL+k+iio= @@ -904,12 +909,16 @@ github.com/gocraft/dbr/v2 v2.7.2/go.mod h1:5bCqyIXO5fYn3jEp/L06QF4K1siFdhxChMjdN github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 h1:uK3X/2mt4tbSGoHvbLBHUny7CKiuwUip3MArtukol4E= github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= +github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= @@ -1021,20 +1030,31 @@ github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hamba/avro/v2 v2.28.0 h1:E8J5D27biyAulWKNiEBhV85QPc9xRMCUCGJewS0KYCE= github.com/hamba/avro/v2 v2.28.0/go.mod h1:9TVrlt1cG1kkTUtm9u2eO5Qb7rZXlYzoKqPt8TSH+TA= +github.com/hashicorp/consul/api v1.15.3/go.mod h1:/g/qgcoBcEXALCNZgRRisyTW0nY86++L0KbeAMXYCeY= +github.com/hashicorp/consul/sdk v0.11.0/go.mod h1:yPkX5Q6CsxTFMjQQDJwzeNmUUF5NUGGbrDsv9wTb8cw= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c= github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6KvJWs= github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4= github.com/hashicorp/go-msgpack/v2 v2.1.1/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhzywQsSRV5033mMu4= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-sockaddr v1.0.5/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI= github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE= github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/raft v1.7.0 h1:4u24Qn6lQ6uwziM++UgsyiT64Q8GyRn43CV41qPiz1o= github.com/hashicorp/raft v1.7.0/go.mod h1:N1sKh6Vn47mrWvEArQgILTyng8GoDRNYlgKyK7PMjs0= github.com/hashicorp/raft-wal v0.4.1 h1:aU8XZ6x8R9BAIB/83Z1dTDtXvDVmv9YVYeXxd/1QBSA= github.com/hashicorp/raft-wal v0.4.1/go.mod h1:A6vP5o8hGOs1LHfC1Okh9xPwWDcmb6Vvuz/QyqUXlOE= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/heroku/x v0.0.61 h1:yfoAAtnFWSFZj+UlS+RZL/h8QYEp1R4wHVEg0G+Hwh4= github.com/heroku/x v0.0.61/go.mod h1:C7xYbpMdond+s6L5VpniDUSVPRwm3kZum1o7XiD5ZHk= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= @@ -1170,14 +1190,17 @@ github.com/mfridman/xflag v0.1.0/go.mod h1:/483ywM5ZO5SuMVjrIGquYNE5CzLrj5Ux/LxW github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mithrandie/readline-csvq v1.3.0 h1:VTJEOGouJ8j27jJCD4kBBbNTxM0OdBvE1aY1tMhlqE8= github.com/mithrandie/readline-csvq v1.3.0/go.mod h1:FKyYqDgf/G4SNov7SMFXRWO6LQLXIOeTog/NB97FZl0= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= @@ -1248,6 +1271,7 @@ github.com/open-telemetry/opentelemetry-collector-contrib/receiver/opencensusrec github.com/open-telemetry/opentelemetry-collector-contrib/receiver/opencensusreceiver v0.124.1/go.mod h1:4+9pSfniXXdRpkKf0QNdElOd7yIWD4ux8D260tSPV54= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.124.1 h1:XkxqUEoukMWXF+EpEWeM9itXKt62yKi13Lzd8ZEASP4= github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.124.1/go.mod h1:CuCZVPz+yn88b5vhZPAlxaMrVuhAVexUV6f8b07lpUc= +github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e/go.mod h1:DYR5Eij8rJl8h7gblRrOZ8g0kW1umSpKqYIBTgeDtLo= github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w= github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU= @@ -1284,6 +1308,7 @@ github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/j github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM= +github.com/prometheus/exporter-toolkit v0.10.1-0.20230714054209-2f4150c63f97/go.mod h1:LoBCZeRh+5hX+fSULNyFnagYlQG/gBsyA/deNzROkq8= github.com/prometheus/statsd_exporter v0.26.1 h1:ucbIAdPmwAUcA+dU+Opok8Qt81Aw8HanlO+2N/Wjv7w= github.com/prometheus/statsd_exporter v0.26.1/go.mod h1:XlDdjAmRmx3JVvPPYuFNUg+Ynyb5kR69iPPkQjxXFMk= github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg= @@ -1308,6 +1333,7 @@ github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1 github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk= github.com/sagikazarmark/crypt v0.6.0 h1:REOEXCs/NFY/1jOCEouMuT4zEniE5YoXbvpC5X/TLF8= github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= @@ -1401,6 +1427,8 @@ github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/uber-go/atomic v1.4.0 h1:yOuPqEq4ovnhEjpHmfFwsqBXDYbQeT6Nb0bwD6XnD5o= github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= +github.com/uber/jaeger-client-go v2.28.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw= @@ -1455,6 +1483,9 @@ gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/9 go.einride.tech/aip v0.68.1 h1:16/AfSxcQISGN5z9C5lM+0mLYXihrHbQ1onvYTr93aQ= go.einride.tech/aip v0.68.1/go.mod h1:XaFtaj4HuA3Zwk9xoBtTWgNubZ0ZZXv9BZJCkuKuWbg= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= go.etcd.io/gofail v0.2.0 h1:p19drv16FKK345a09a1iubchlw/vmRuksmRzgBIGjcA= go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o= go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= @@ -1582,6 +1613,7 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= @@ -1594,6 +1626,7 @@ go.opentelemetry.io/contrib/otelconf v0.15.0 h1:BLNiIUsrNcqhSKpsa6CnhE6LdrpY1A8X go.opentelemetry.io/contrib/otelconf v0.15.0/go.mod h1:OPH1seO5z9dp1P26gnLtoM9ht7JDvh3Ws6XRHuXqImY= go.opentelemetry.io/contrib/propagators/b3 v1.35.0 h1:DpwKW04LkdFRFCIgM3sqwTJA/QREHMeMHYPWP1WeaPQ= go.opentelemetry.io/contrib/propagators/b3 v1.35.0/go.mod h1:9+SNxwqvCWo1qQwUpACBY5YKNVxFJn5mlbXg/4+uKBg= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0/go.mod h1:0ciyFyYZxE6JqRAQvIgGRabKWDUmNdW3GAQb6y/RlFU= go.opentelemetry.io/contrib/zpages v0.60.0 h1:wOM9ie1Hz4H88L9KE6GrGbKJhfm+8F1NfW/Y3q9Xt+8= go.opentelemetry.io/contrib/zpages v0.60.0/go.mod h1:xqfToSRGh2MYUsfyErNz8jnNDPlnpZqWM/y6Z2Cx7xw= go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= @@ -1612,6 +1645,7 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6o go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ= go.opentelemetry.io/otel/exporters/prometheus v0.58.0/go.mod h1:7qo/4CLI+zYSNbv0GMNquzuss2FVZo3OYrGh96n4HNc= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY= @@ -1642,6 +1676,7 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -1658,10 +1693,12 @@ golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZv golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= @@ -1675,9 +1712,11 @@ golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20190921015927-1a5e07d1ff72/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= @@ -1702,6 +1741,7 @@ golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -1759,6 +1799,7 @@ google.golang.org/api v0.229.0/go.mod h1:wyDfmq5g1wYJWn29O22FDWN48P7Xcz0xz+LBppt google.golang.org/api v0.232.0/go.mod h1:p9QCfBWZk1IJETUdbTKloR5ToFdKbYh2fkjsUL6vNoY= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= google.golang.org/genproto v0.0.0-20250106144421-5f5ef82da422/go.mod h1:1NPAxoesyw/SgLPqaUp9u1f9PWCLAk/jVmhx7gJZStg= @@ -1781,15 +1822,18 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go. google.golang.org/genproto/googleapis/api v0.0.0-20250425173222-7b384671a197/go.mod h1:Cd8IzgPo5Akum2c9R6FsXNaZbH3Jpa2gpHlW89FqlyQ= google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw= google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:W3S/3np0/dPWsWLi1h/UymYctGXaGBM2StwzD0y140U= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250505200425-f936aa4a68b2 h1:DbpkGFGRkd4GORg+IWQW2EhxUaa/My/PM8d1CGyTDMY= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250512202823-5a2f75b736a9 h1:YI36gCL8AQMhzYN6+jH8PdV/iZ0On+Zd0rO/7lCH3k8= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= @@ -1802,7 +1846,9 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= diff --git a/pkg/services/annotations/annotationsimpl/loki/historian_store.go b/pkg/services/annotations/annotationsimpl/loki/historian_store.go index 59ed1494c41..cfbad0f1342 100644 --- a/pkg/services/annotations/annotationsimpl/loki/historian_store.go +++ b/pkg/services/annotations/annotationsimpl/loki/historian_store.go @@ -8,7 +8,8 @@ import ( "sort" "time" - "github.com/grafana/grafana/pkg/services/ngalert/lokiclient" + "github.com/grafana/alerting/lokiclient" + "github.com/grafana/grafana/pkg/services/ngalert/lokiconfig" "golang.org/x/exp/constraints" "github.com/grafana/grafana/pkg/components/simplejson" @@ -61,7 +62,7 @@ func NewLokiHistorianStore(cfg setting.UnifiedAlertingStateHistorySettings, db d if !useStore(cfg) { return nil } - lokiCfg, err := lokiclient.NewLokiConfig(cfg.LokiSettings) + lokiCfg, err := lokiconfig.NewLokiConfig(cfg.LokiSettings) if err != nil { // this config error is already handled elsewhere return nil diff --git a/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go b/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go index 12ce9254cf1..acf8168fb3f 100644 --- a/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go +++ b/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go @@ -10,11 +10,12 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/services/ngalert/lokiclient" + "github.com/grafana/alerting/lokiclient" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" "golang.org/x/exp/maps" + "github.com/grafana/alerting/client" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" @@ -23,7 +24,6 @@ import ( "github.com/grafana/grafana/pkg/services/annotations/testutil" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/ngalert/client" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" diff --git a/pkg/services/ngalert/client/client.go b/pkg/services/ngalert/client/client.go deleted file mode 100644 index b6c99d32be4..00000000000 --- a/pkg/services/ngalert/client/client.go +++ /dev/null @@ -1,125 +0,0 @@ -package client - -import ( - "context" - "fmt" - "net/http" - "strconv" - - "github.com/grafana/dskit/instrument" - "github.com/grafana/grafana/pkg/infra/tracing" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - semconv "go.opentelemetry.io/otel/semconv/v1.17.0" - "go.opentelemetry.io/otel/trace" -) - -// Requester executes an HTTP request. -type Requester interface { - Do(req *http.Request) (*http.Response, error) -} - -// TimedClient instruments a request with metrics. It implements Requester. -type TimedClient struct { - client Requester - collector instrument.Collector -} - -type contextKey int - -// OperationNameContextKey specifies the operation name location within the context -// for instrumentation. -const OperationNameContextKey contextKey = 0 - -// NewTimedClient creates a Requester that instruments requests on `client`. -func NewTimedClient(client Requester, collector instrument.Collector) *TimedClient { - return &TimedClient{ - client: client, - collector: collector, - } -} - -// Do executes the request. -func (c TimedClient) Do(r *http.Request) (*http.Response, error) { - return TimeRequest(r.Context(), c.operationName(r), c.collector, c.client, r) -} - -// RoundTrip implements the RoundTripper interface. -func (c TimedClient) RoundTrip(r *http.Request) (*http.Response, error) { - return c.Do(r) -} - -func (c TimedClient) operationName(r *http.Request) string { - operation, _ := r.Context().Value(OperationNameContextKey).(string) - if operation == "" { - operation = r.URL.Path - } - return operation -} - -// TimeRequest performs an HTTP client request and records the duration in a histogram. -func TimeRequest(ctx context.Context, operation string, coll instrument.Collector, client Requester, request *http.Request) (*http.Response, error) { - var response *http.Response - doRequest := func(_ context.Context) error { - var err error - response, err = client.Do(request) // nolint:bodyclose - return err - } - toStatusCode := func(err error) string { - if err == nil { - return strconv.Itoa(response.StatusCode) - } - return "error" - } - err := instrument.CollectedRequest(ctx, fmt.Sprintf("%s %s", request.Method, operation), - coll, toStatusCode, doRequest) - return response, err -} - -// TracedClient instruments a request with tracing. It implements Requester. -type TracedClient struct { - client Requester - tracer tracing.Tracer - name string -} - -func NewTracedClient(client Requester, tracer tracing.Tracer, name string) *TracedClient { - return &TracedClient{ - client: client, - tracer: tracer, - name: name, - } -} - -// Do executes the request. -func (c TracedClient) Do(r *http.Request) (*http.Response, error) { - ctx, span := c.tracer.Start(r.Context(), c.name, trace.WithSpanKind(trace.SpanKindClient)) - defer span.End() - - span.SetAttributes(semconv.HTTPURL(r.URL.String())) - span.SetAttributes(semconv.HTTPMethod(r.Method)) - - c.tracer.Inject(ctx, r.Header, span) - - r = r.WithContext(ctx) - resp, err := c.client.Do(r) - if err != nil { - span.SetStatus(codes.Error, "request failed") - span.RecordError(err) - } else { - if resp.ContentLength > 0 { - span.SetAttributes(attribute.Int64("http.content_length", resp.ContentLength)) - } - span.SetAttributes(semconv.HTTPStatusCode(resp.StatusCode)) - if resp.StatusCode >= 400 && resp.StatusCode < 600 { - span.RecordError(fmt.Errorf("error with HTTP status code %d", resp.StatusCode)) - } - } - - return resp, err -} - -// RoundTrip implements the RoundTripper interface. -func (c TracedClient) RoundTrip(r *http.Request) (*http.Response, error) { - return c.Do(r) -} diff --git a/pkg/services/ngalert/client/client_test.go b/pkg/services/ngalert/client/client_test.go deleted file mode 100644 index 25d26576e66..00000000000 --- a/pkg/services/ngalert/client/client_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package client - -import ( - "context" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestTimedClient_operationName(t *testing.T) { - r, err := http.NewRequest("GET", "https://weave.test", nil) - assert.NoError(t, err) - - r = r.WithContext(context.WithValue(context.Background(), OperationNameContextKey, "opp")) - c := NewTimedClient(http.DefaultClient, nil) - - assert.Equal(t, "opp", c.operationName(r)) -} - -func TestTimedClient_operationName_Default(t *testing.T) { - r, err := http.NewRequest("GET", "https://weave.test/you/know/me", nil) - assert.NoError(t, err) - - r = r.WithContext(context.Background()) - c := NewTimedClient(http.DefaultClient, nil) - - assert.Equal(t, "/you/know/me", c.operationName(r)) -} diff --git a/pkg/services/ngalert/lokiclient/client.go b/pkg/services/ngalert/lokiclient/client.go deleted file mode 100644 index 0b1fca30daf..00000000000 --- a/pkg/services/ngalert/lokiclient/client.go +++ /dev/null @@ -1,333 +0,0 @@ -package lokiclient - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/grafana/dskit/instrument" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/ngalert/client" - "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" -) - -const defaultPageSize = 1000 -const maximumPageSize = 5000 - -func NewRequester() client.Requester { - return &http.Client{} -} - -// encoder serializes log streams to some byte format. -type encoder interface { - // encode serializes a set of log streams to bytes. - encode(s []Stream) ([]byte, error) - // headers returns a set of HTTP-style headers that describes the encoding scheme used. - headers() map[string]string -} - -type LokiConfig struct { - ReadPathURL *url.URL - WritePathURL *url.URL - BasicAuthUser string - BasicAuthPassword string - TenantID string - ExternalLabels map[string]string - Encoder encoder - MaxQueryLength time.Duration - MaxQuerySize int -} - -func NewLokiConfig(cfg setting.UnifiedAlertingLokiSettings) (LokiConfig, error) { - read, write := cfg.LokiReadURL, cfg.LokiWriteURL - if read == "" { - read = cfg.LokiRemoteURL - } - if write == "" { - write = cfg.LokiRemoteURL - } - - if read == "" { - return LokiConfig{}, fmt.Errorf("either read path URL or remote Loki URL must be provided") - } - if write == "" { - return LokiConfig{}, fmt.Errorf("either write path URL or remote Loki URL must be provided") - } - - readURL, err := url.Parse(read) - if err != nil { - return LokiConfig{}, fmt.Errorf("failed to parse loki remote read URL: %w", err) - } - writeURL, err := url.Parse(write) - if err != nil { - return LokiConfig{}, fmt.Errorf("failed to parse loki remote write URL: %w", err) - } - - return LokiConfig{ - ReadPathURL: readURL, - WritePathURL: writeURL, - BasicAuthUser: cfg.LokiBasicAuthUsername, - BasicAuthPassword: cfg.LokiBasicAuthPassword, - TenantID: cfg.LokiTenantID, - ExternalLabels: cfg.ExternalLabels, - MaxQueryLength: cfg.LokiMaxQueryLength, - MaxQuerySize: cfg.LokiMaxQuerySize, - // Snappy-compressed protobuf is the default, same goes for Promtail. - Encoder: SnappyProtoEncoder{}, - }, nil -} - -type HttpLokiClient struct { - client client.Requester - encoder encoder - cfg LokiConfig - bytesWritten prometheus.Counter - log log.Logger -} - -// Kind of Operation (=, !=, =~, !~) -type Operator string - -const ( - // Equal operator (=) - Eq Operator = "=" - // Not Equal operator (!=) - Neq Operator = "!=" - // Equal operator supporting RegEx (=~) - EqRegEx Operator = "=~" - // Not Equal operator supporting RegEx (!~) - NeqRegEx Operator = "!~" -) - -func NewLokiClient(cfg LokiConfig, req client.Requester, bytesWritten prometheus.Counter, writeDuration *instrument.HistogramCollector, logger log.Logger, tracer tracing.Tracer, spanName string) *HttpLokiClient { - tc := client.NewTimedClient(req, writeDuration) - trc := client.NewTracedClient(tc, tracer, spanName) - return &HttpLokiClient{ - client: trc, - encoder: cfg.Encoder, - cfg: cfg, - bytesWritten: bytesWritten, - log: logger.New("protocol", "http"), - } -} - -func (c *HttpLokiClient) Ping(ctx context.Context) error { - log := c.log.FromContext(ctx) - uri := c.cfg.ReadPathURL.JoinPath("/loki/api/v1/labels") - req, err := http.NewRequest(http.MethodGet, uri.String(), nil) - if err != nil { - return fmt.Errorf("error creating request: %w", err) - } - c.setAuthAndTenantHeaders(req) - - req = req.WithContext(ctx) - res, err := c.client.Do(req) - if res != nil { - defer func() { - if err := res.Body.Close(); err != nil { - log.Warn("Failed to close response body", "err", err) - } - }() - } - if err != nil { - return fmt.Errorf("error sending request: %w", err) - } - - if res.StatusCode < 200 || res.StatusCode >= 300 { - return fmt.Errorf("ping request to loki endpoint returned a non-200 status code: %d", res.StatusCode) - } - log.Debug("Ping request to Loki endpoint succeeded", "status", res.StatusCode) - return nil -} - -type Stream struct { - Stream map[string]string `json:"stream"` - Values []Sample `json:"values"` -} - -type Sample struct { - T time.Time - V string -} - -func (r *Sample) MarshalJSON() ([]byte, error) { - return json.Marshal([2]string{ - fmt.Sprintf("%d", r.T.UnixNano()), r.V, - }) -} - -func (r *Sample) UnmarshalJSON(b []byte) error { - // A Loki stream sample is formatted like a list with two elements, [At, Val] - // At is a string wrapping a timestamp, in nanosecond unix epoch. - // Val is a string containing the log line. - var tuple [2]string - if err := json.Unmarshal(b, &tuple); err != nil { - return fmt.Errorf("failed to deserialize sample in Loki response: %w", err) - } - nano, err := strconv.ParseInt(tuple[0], 10, 64) - if err != nil { - return fmt.Errorf("timestamp in Loki sample not convertible to nanosecond epoch: %v", tuple[0]) - } - r.T = time.Unix(0, nano) - r.V = tuple[1] - return nil -} - -func (c *HttpLokiClient) Push(ctx context.Context, s []Stream) error { - log := c.log.FromContext(ctx) - enc, err := c.encoder.encode(s) - if err != nil { - return err - } - - uri := c.cfg.WritePathURL.JoinPath("/loki/api/v1/push") - req, err := http.NewRequest(http.MethodPost, uri.String(), bytes.NewBuffer(enc)) - if err != nil { - return fmt.Errorf("failed to create Loki request: %w", err) - } - - c.setAuthAndTenantHeaders(req) - for k, v := range c.encoder.headers() { - req.Header.Add(k, v) - } - - c.bytesWritten.Add(float64(len(enc))) - req = req.WithContext(ctx) - resp, err := c.client.Do(req) - if err != nil { - return fmt.Errorf("failed to send request: %w", err) - } - defer func() { - if err := resp.Body.Close(); err != nil { - log.Warn("Failed to close response body", "err", err) - } - }() - - _, err = c.handleLokiResponse(log, resp) - if err != nil { - return err - } - - return nil -} - -func (c *HttpLokiClient) setAuthAndTenantHeaders(req *http.Request) { - if c.cfg.BasicAuthUser != "" || c.cfg.BasicAuthPassword != "" { - req.SetBasicAuth(c.cfg.BasicAuthUser, c.cfg.BasicAuthPassword) - } - - if c.cfg.TenantID != "" { - req.Header.Add("X-Scope-OrgID", c.cfg.TenantID) - } -} - -func (c *HttpLokiClient) RangeQuery(ctx context.Context, logQL string, start, end, limit int64) (QueryRes, error) { - log := c.log.FromContext(ctx) - // Run the pre-flight checks for the query. - if start > end { - return QueryRes{}, fmt.Errorf("start time cannot be after end time") - } - start, end = ClampRange(start, end, c.cfg.MaxQueryLength.Nanoseconds()) - if limit < 1 { - limit = defaultPageSize - } - if limit > maximumPageSize { - limit = maximumPageSize - } - - queryURL := c.cfg.ReadPathURL.JoinPath("/loki/api/v1/query_range") - - values := url.Values{} - values.Set("query", logQL) - values.Set("start", fmt.Sprintf("%d", start)) - values.Set("end", fmt.Sprintf("%d", end)) - values.Set("limit", fmt.Sprintf("%d", limit)) - - queryURL.RawQuery = values.Encode() - log.Debug("Sending query request", "query", logQL, "start", start, "end", end, "limit", limit) - req, err := http.NewRequest(http.MethodGet, - queryURL.String(), nil) - if err != nil { - return QueryRes{}, fmt.Errorf("error creating request: %w", err) - } - - req = req.WithContext(ctx) - c.setAuthAndTenantHeaders(req) - - res, err := c.client.Do(req) - if err != nil { - return QueryRes{}, fmt.Errorf("error executing request: %w", err) - } - defer func() { - if err := res.Body.Close(); err != nil { - log.Warn("Failed to close response body", "err", err) - } - }() - - data, err := c.handleLokiResponse(log, res) - if err != nil { - return QueryRes{}, err - } - - result := QueryRes{} - err = json.Unmarshal(data, &result) - if err != nil { - fmt.Println(string(data)) - return QueryRes{}, fmt.Errorf("error parsing request response: %w", err) - } - - return result, nil -} - -func (c *HttpLokiClient) MaxQuerySize() int { - return c.cfg.MaxQuerySize -} - -type QueryRes struct { - Data QueryData `json:"data"` -} - -type QueryData struct { - Result []Stream `json:"result"` -} - -func (c *HttpLokiClient) handleLokiResponse(log log.Logger, res *http.Response) ([]byte, error) { - if res == nil { - return nil, fmt.Errorf("response is nil") - } - - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("error reading request response: %w", err) - } - - if res.StatusCode < 200 || res.StatusCode >= 300 { - if len(data) > 0 { - log.Error("Error response from Loki", "response", string(data), "status", res.StatusCode) - } else { - log.Error("Error response from Loki with an empty body", "status", res.StatusCode) - } - return nil, fmt.Errorf("received a non-200 response from loki, status: %d", res.StatusCode) - } - - return data, nil -} - -// ClampRange ensures that the time range is within the configured maximum query length. -func ClampRange(start, end, maxTimeRange int64) (newStart int64, newEnd int64) { - newStart, newEnd = start, end - - if maxTimeRange != 0 && end-start > maxTimeRange { - newStart = end - maxTimeRange - } - - return newStart, newEnd -} diff --git a/pkg/services/ngalert/lokiclient/client_test.go b/pkg/services/ngalert/lokiclient/client_test.go deleted file mode 100644 index 5568ec1601c..00000000000 --- a/pkg/services/ngalert/lokiclient/client_test.go +++ /dev/null @@ -1,409 +0,0 @@ -package lokiclient - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "testing" - "time" - - "github.com/grafana/grafana/pkg/services/ngalert/client" - "github.com/grafana/grafana/pkg/services/ngalert/metrics" - "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/tracing" -) - -const lokiClientSpanName = "testLokiClientSpanName" - -func TestLokiConfig(t *testing.T) { - t.Run("test URL options", func(t *testing.T) { - type testCase struct { - name string - in setting.UnifiedAlertingLokiSettings - expRead string - expWrite string - expErr string - } - - cases := []testCase{ - { - name: "remote url only", - in: setting.UnifiedAlertingLokiSettings{ - LokiRemoteURL: "http://url.com", - }, - expRead: "http://url.com", - expWrite: "http://url.com", - }, - { - name: "separate urls", - in: setting.UnifiedAlertingLokiSettings{ - LokiReadURL: "http://read.url.com", - LokiWriteURL: "http://write.url.com", - }, - expRead: "http://read.url.com", - expWrite: "http://write.url.com", - }, - { - name: "single fallback", - in: setting.UnifiedAlertingLokiSettings{ - LokiRemoteURL: "http://url.com", - LokiReadURL: "http://read.url.com", - }, - expRead: "http://read.url.com", - expWrite: "http://url.com", - }, - { - name: "missing read", - in: setting.UnifiedAlertingLokiSettings{ - LokiWriteURL: "http://url.com", - }, - expErr: "either read path URL or remote", - }, - { - name: "missing write", - in: setting.UnifiedAlertingLokiSettings{ - LokiReadURL: "http://url.com", - }, - expErr: "either write path URL or remote", - }, - { - name: "invalid", - in: setting.UnifiedAlertingLokiSettings{ - LokiRemoteURL: "://://", - }, - expErr: "failed to parse", - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - res, err := NewLokiConfig(tc.in) - if tc.expErr != "" { - require.ErrorContains(t, err, tc.expErr) - } else { - require.Equal(t, tc.expRead, res.ReadPathURL.String()) - require.Equal(t, tc.expWrite, res.WritePathURL.String()) - } - }) - } - }) - - t.Run("captures external labels", func(t *testing.T) { - set := setting.UnifiedAlertingLokiSettings{ - LokiRemoteURL: "http://url.com", - ExternalLabels: map[string]string{"a": "b"}, - } - - res, err := NewLokiConfig(set) - - require.NoError(t, err) - require.Contains(t, res.ExternalLabels, "a") - }) -} - -func TestLokiHTTPClient(t *testing.T) { - t.Run("push formats expected data", func(t *testing.T) { - req := NewFakeRequester() - client := createTestLokiClient(req) - now := time.Now().UTC() - data := []Stream{ - { - Stream: map[string]string{}, - Values: []Sample{ - { - T: now, - V: "some line", - }, - }, - }, - } - - err := client.Push(context.Background(), data) - - require.NoError(t, err) - require.Contains(t, "/loki/api/v1/push", req.LastRequest.URL.Path) - sent := reqBody(t, req.LastRequest) - exp := fmt.Sprintf(`{"streams": [{"stream": {}, "values": [["%d", "some line"]]}]}`, now.UnixNano()) - require.JSONEq(t, exp, sent) - }) - - t.Run("range query", func(t *testing.T) { - t.Run("passes along page size", func(t *testing.T) { - req := NewFakeRequester().WithResponse(&http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{}`)), - ContentLength: int64(0), - Header: make(http.Header, 0), - }) - client := createTestLokiClient(req) - now := time.Now().UTC().UnixNano() - q := `{from="state-history"}` - - _, err := client.RangeQuery(context.Background(), q, now-100, now, 1100) - - require.NoError(t, err) - params := req.LastRequest.URL.Query() - require.True(t, params.Has("limit"), "query params did not contain 'limit': %#v", params) - require.Equal(t, fmt.Sprint(1100), params.Get("limit")) - }) - - t.Run("uses default page size if limit not provided", func(t *testing.T) { - req := NewFakeRequester().WithResponse(&http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{}`)), - ContentLength: int64(0), - Header: make(http.Header, 0), - }) - client := createTestLokiClient(req) - now := time.Now().UTC().UnixNano() - q := `{from="state-history"}` - - _, err := client.RangeQuery(context.Background(), q, now-100, now, 0) - - require.NoError(t, err) - params := req.LastRequest.URL.Query() - require.True(t, params.Has("limit"), "query params did not contain 'limit': %#v", params) - require.Equal(t, fmt.Sprint(defaultPageSize), params.Get("limit")) - }) - - t.Run("uses default page size if limit invalid", func(t *testing.T) { - req := NewFakeRequester().WithResponse(&http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{}`)), - ContentLength: int64(0), - Header: make(http.Header, 0), - }) - client := createTestLokiClient(req) - now := time.Now().UTC().UnixNano() - q := `{from="state-history"}` - - _, err := client.RangeQuery(context.Background(), q, now-100, now, -100) - - require.NoError(t, err) - params := req.LastRequest.URL.Query() - require.True(t, params.Has("limit"), "query params did not contain 'limit': %#v", params) - require.Equal(t, fmt.Sprint(defaultPageSize), params.Get("limit")) - }) - - t.Run("uses maximum page size if limit too big", func(t *testing.T) { - req := NewFakeRequester().WithResponse(&http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{}`)), - ContentLength: int64(0), - Header: make(http.Header, 0), - }) - client := createTestLokiClient(req) - now := time.Now().UTC().UnixNano() - q := `{from="state-history"}` - - _, err := client.RangeQuery(context.Background(), q, now-100, now, maximumPageSize+1000) - - require.NoError(t, err) - params := req.LastRequest.URL.Query() - require.True(t, params.Has("limit"), "query params did not contain 'limit': %#v", params) - require.Equal(t, fmt.Sprint(maximumPageSize), params.Get("limit")) - }) - }) -} - -// This function can be used for local testing, just remove the skip call. -func TestLokiHTTPClient_Manual(t *testing.T) { - t.Skip() - - t.Run("smoke test pinging Loki", func(t *testing.T) { - url, err := url.Parse("https://logs-prod-eu-west-0.grafana.net") - require.NoError(t, err) - - metrics := metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem) - client := NewLokiClient(LokiConfig{ - ReadPathURL: url, - WritePathURL: url, - Encoder: JsonEncoder{}, - }, NewRequester(), metrics.BytesWritten, metrics.WriteDuration, log.NewNopLogger(), tracing.InitializeTracerForTest(), lokiClientSpanName) - - // Unauthorized request should fail against Grafana Cloud. - err = client.Ping(context.Background()) - require.Error(t, err) - - client.cfg.BasicAuthUser = "" - client.cfg.BasicAuthPassword = "" - - // When running on prem, you might need to set the tenant id, - // so the x-scope-orgid header is set. - // client.cfg.TenantID = "" - - // Authorized request should not fail against Grafana Cloud. - err = client.Ping(context.Background()) - require.NoError(t, err) - }) - - t.Run("smoke test range querying Loki", func(t *testing.T) { - url, err := url.Parse("https://logs-prod-eu-west-0.grafana.net") - require.NoError(t, err) - - metrics := metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem) - client := NewLokiClient(LokiConfig{ - ReadPathURL: url, - WritePathURL: url, - BasicAuthUser: "", - BasicAuthPassword: "", - Encoder: JsonEncoder{}, - }, NewRequester(), metrics.BytesWritten, metrics.WriteDuration, log.NewNopLogger(), tracing.InitializeTracerForTest(), lokiClientSpanName) - - // When running on prem, you might need to set the tenant id, - // so the x-scope-orgid header is set. - // client.cfg.TenantID = "" - - logQL := `{probe="Paris"}` - - // Define the query time range - start := time.Now().Add(-30 * time.Minute).UnixNano() - end := time.Now().UnixNano() - - // Authorized request should not fail against Grafana Cloud. - res, err := client.RangeQuery(context.Background(), logQL, start, end, defaultPageSize) - require.NoError(t, err) - require.NotNil(t, res) - }) -} - -func TestRow(t *testing.T) { - t.Run("marshal", func(t *testing.T) { - row := Sample{ - T: time.Unix(0, 1234), - V: "some sample", - } - - jsn, err := json.Marshal(&row) - - require.NoError(t, err) - require.JSONEq(t, `["1234", "some sample"]`, string(jsn)) - }) - - t.Run("unmarshal", func(t *testing.T) { - jsn := []byte(`["1234", "some sample"]`) - - row := Sample{} - err := json.Unmarshal(jsn, &row) - - require.NoError(t, err) - require.Equal(t, int64(1234), row.T.UnixNano()) - require.Equal(t, "some sample", row.V) - }) - - t.Run("unmarshal invalid", func(t *testing.T) { - jsn := []byte(`{"key": "wrong shape"}`) - - row := Sample{} - err := json.Unmarshal(jsn, &row) - - require.ErrorContains(t, err, "failed to deserialize sample") - }) - - t.Run("unmarshal bad timestamp", func(t *testing.T) { - jsn := []byte(`["not-unix-nano", "some sample"]`) - - row := Sample{} - err := json.Unmarshal(jsn, &row) - - require.ErrorContains(t, err, "timestamp in Loki sample") - }) -} - -func TestStream(t *testing.T) { - t.Run("marshal", func(t *testing.T) { - stream := Stream{ - Stream: map[string]string{"a": "b"}, - Values: []Sample{ - {T: time.Unix(0, 1), V: "one"}, - {T: time.Unix(0, 2), V: "two"}, - }, - } - - jsn, err := json.Marshal(stream) - - require.NoError(t, err) - require.JSONEq( - t, - `{"stream": {"a": "b"}, "values": [["1", "one"], ["2", "two"]]}`, - string(jsn), - ) - }) -} - -func TestClampRange(t *testing.T) { - tc := []struct { - name string - oldRange []int64 - max int64 - newRange []int64 - }{ - { - name: "clamps start value if max is smaller than range", - oldRange: []int64{5, 10}, - max: 1, - newRange: []int64{9, 10}, - }, - { - name: "returns same values if max is greater than range", - oldRange: []int64{5, 10}, - max: 20, - newRange: []int64{5, 10}, - }, - { - name: "returns same values if max is equal to range", - oldRange: []int64{5, 10}, - max: 5, - newRange: []int64{5, 10}, - }, - { - name: "returns same values if max is zero", - oldRange: []int64{5, 10}, - max: 0, - newRange: []int64{5, 10}, - }, - } - - for _, c := range tc { - t.Run(c.name, func(t *testing.T) { - start, end := ClampRange(c.oldRange[0], c.oldRange[1], c.max) - - require.Equal(t, c.newRange[0], start) - require.Equal(t, c.newRange[1], end) - }) - } -} - -func createTestLokiClient(req client.Requester) *HttpLokiClient { - url, _ := url.Parse("http://some.url") - cfg := LokiConfig{ - WritePathURL: url, - ReadPathURL: url, - Encoder: JsonEncoder{}, - } - met := metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem) - return NewLokiClient(cfg, req, met.BytesWritten, met.WriteDuration, log.NewNopLogger(), tracing.InitializeTracerForTest(), lokiClientSpanName) -} - -func reqBody(t *testing.T, req *http.Request) string { - t.Helper() - - defer func() { - _ = req.Body.Close() - }() - byt, err := io.ReadAll(req.Body) - require.NoError(t, err) - return string(byt) -} diff --git a/pkg/services/ngalert/lokiclient/encode.go b/pkg/services/ngalert/lokiclient/encode.go deleted file mode 100644 index 16dd827f6c5..00000000000 --- a/pkg/services/ngalert/lokiclient/encode.go +++ /dev/null @@ -1,107 +0,0 @@ -package lokiclient - -import ( - "encoding/json" - "fmt" - "slices" - "strconv" - "strings" - - "github.com/gogo/protobuf/proto" - "github.com/golang/snappy" - "github.com/prometheus/common/model" - - "github.com/grafana/grafana/pkg/components/loki/logproto" -) - -type JsonEncoder struct{} - -func (e JsonEncoder) encode(s []Stream) ([]byte, error) { - body := struct { - Streams []Stream `json:"streams"` - }{Streams: s} - enc, err := json.Marshal(body) - if err != nil { - return nil, fmt.Errorf("failed to serialize Loki payload: %w", err) - } - return enc, nil -} - -func (e JsonEncoder) headers() map[string]string { - return map[string]string{ - "Content-Type": "application/json", - } -} - -type SnappyProtoEncoder struct{} - -func (e SnappyProtoEncoder) encode(s []Stream) ([]byte, error) { - body := logproto.PushRequest{ - Streams: make([]logproto.Stream, 0, len(s)), - } - - for _, str := range s { - entries := make([]logproto.Entry, 0, len(str.Values)) - for _, sample := range str.Values { - entries = append(entries, logproto.Entry{ - Timestamp: sample.T, - Line: sample.V, - }) - } - body.Streams = append(body.Streams, logproto.Stream{ - Labels: labelsMapToString(str.Stream, ""), - Entries: entries, - // Hash seems to be mainly used for query responses. Promtail does not seem to calculate this field on push. - }) - } - - buf, err := proto.Marshal(&body) - if err != nil { - return nil, fmt.Errorf("failed to serialize Loki payload to proto: %w", err) - } - buf = snappy.Encode(nil, buf) - return buf, nil -} - -func (e SnappyProtoEncoder) headers() map[string]string { - return map[string]string{ - "Content-Type": "application/x-protobuf", - "Content-Encoding": "snappy", - } -} - -// Copied from promtail. -// Modified slightly to work in terms of plain map[string]string to avoid some unnecessary copies and type casts. -// TODO: pkg/components/loki/lokihttp/batch.go contains an older (loki 2.7.4 released) version of this. -// TODO: Consider replacing that one, with this one. -func labelsMapToString(ls map[string]string, without model.LabelName) string { - var b strings.Builder - totalSize := 2 - lstrs := make([]string, 0, len(ls)) - - for l, v := range ls { - if l == string(without) { - continue - } - - lstrs = append(lstrs, l) - // guess size increase: 2 for `, ` between labels and 3 for the `=` and quotes around label value - totalSize += len(l) + 2 + len(v) + 3 - } - - b.Grow(totalSize) - b.WriteByte('{') - slices.Sort(lstrs) - for i, l := range lstrs { - if i > 0 { - b.WriteString(", ") - } - - b.WriteString(l) - b.WriteString(`=`) - b.WriteString(strconv.Quote(ls[l])) - } - b.WriteByte('}') - - return b.String() -} diff --git a/pkg/services/ngalert/lokiclient/testing.go b/pkg/services/ngalert/lokiclient/testing.go deleted file mode 100644 index 8d3cd097f0c..00000000000 --- a/pkg/services/ngalert/lokiclient/testing.go +++ /dev/null @@ -1,45 +0,0 @@ -package lokiclient - -import ( - "bytes" - "io" - "net/http" -) - -type FakeRequester struct { - LastRequest *http.Request - Resp *http.Response -} - -func NewFakeRequester() *FakeRequester { - return &FakeRequester{ - Resp: &http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString("")), - ContentLength: int64(0), - Header: make(http.Header, 0), - }, - } -} - -func (f *FakeRequester) WithResponse(resp *http.Response) *FakeRequester { - f.Resp = resp - return f -} - -func (f *FakeRequester) Do(req *http.Request) (*http.Response, error) { - f.LastRequest = req - f.Resp.Request = req // Not concurrency-safe! - return f.Resp, nil -} - -func BadResponse() *http.Response { - return &http.Response{ - Status: "400 Bad Request", - StatusCode: http.StatusBadRequest, - Body: io.NopCloser(bytes.NewBufferString("")), - ContentLength: int64(0), - Header: make(http.Header, 0), - } -} diff --git a/pkg/services/ngalert/lokiconfig/lokiconfig.go b/pkg/services/ngalert/lokiconfig/lokiconfig.go new file mode 100644 index 00000000000..ccec08c0035 --- /dev/null +++ b/pkg/services/ngalert/lokiconfig/lokiconfig.go @@ -0,0 +1,48 @@ +package lokiconfig + +import ( + "fmt" + "net/url" + + "github.com/grafana/alerting/lokiclient" + "github.com/grafana/grafana/pkg/setting" +) + +func NewLokiConfig(cfg setting.UnifiedAlertingLokiSettings) (lokiclient.LokiConfig, error) { + read, write := cfg.LokiReadURL, cfg.LokiWriteURL + if read == "" { + read = cfg.LokiRemoteURL + } + if write == "" { + write = cfg.LokiRemoteURL + } + + if read == "" { + return lokiclient.LokiConfig{}, fmt.Errorf("either read path URL or remote Loki URL must be provided") + } + if write == "" { + return lokiclient.LokiConfig{}, fmt.Errorf("either write path URL or remote Loki URL must be provided") + } + + readURL, err := url.Parse(read) + if err != nil { + return lokiclient.LokiConfig{}, fmt.Errorf("failed to parse loki remote read URL: %w", err) + } + writeURL, err := url.Parse(write) + if err != nil { + return lokiclient.LokiConfig{}, fmt.Errorf("failed to parse loki remote write URL: %w", err) + } + + return lokiclient.LokiConfig{ + ReadPathURL: readURL, + WritePathURL: writeURL, + BasicAuthUser: cfg.LokiBasicAuthUsername, + BasicAuthPassword: cfg.LokiBasicAuthPassword, + TenantID: cfg.LokiTenantID, + ExternalLabels: cfg.ExternalLabels, + MaxQueryLength: cfg.LokiMaxQueryLength, + MaxQuerySize: cfg.LokiMaxQuerySize, + // Snappy-compressed protobuf is the default, same goes for Promtail. + Encoder: lokiclient.SnappyProtoEncoder{}, + }, nil +} diff --git a/pkg/services/ngalert/lokiconfig/lokiconfig_test.go b/pkg/services/ngalert/lokiconfig/lokiconfig_test.go new file mode 100644 index 00000000000..7c2bc0ef1b8 --- /dev/null +++ b/pkg/services/ngalert/lokiconfig/lokiconfig_test.go @@ -0,0 +1,94 @@ +package lokiconfig + +import ( + "testing" + + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/require" +) + +func TestLokiConfig(t *testing.T) { + t.Run("test URL options", func(t *testing.T) { + type testCase struct { + name string + in setting.UnifiedAlertingLokiSettings + expRead string + expWrite string + expErr string + } + + cases := []testCase{ + { + name: "remote url only", + in: setting.UnifiedAlertingLokiSettings{ + LokiRemoteURL: "http://url.com", + }, + expRead: "http://url.com", + expWrite: "http://url.com", + }, + { + name: "separate urls", + in: setting.UnifiedAlertingLokiSettings{ + LokiReadURL: "http://read.url.com", + LokiWriteURL: "http://write.url.com", + }, + expRead: "http://read.url.com", + expWrite: "http://write.url.com", + }, + { + name: "single fallback", + in: setting.UnifiedAlertingLokiSettings{ + LokiRemoteURL: "http://url.com", + LokiReadURL: "http://read.url.com", + }, + expRead: "http://read.url.com", + expWrite: "http://url.com", + }, + { + name: "missing read", + in: setting.UnifiedAlertingLokiSettings{ + LokiWriteURL: "http://url.com", + }, + expErr: "either read path URL or remote", + }, + { + name: "missing write", + in: setting.UnifiedAlertingLokiSettings{ + LokiReadURL: "http://url.com", + }, + expErr: "either write path URL or remote", + }, + { + name: "invalid", + in: setting.UnifiedAlertingLokiSettings{ + LokiRemoteURL: "://://", + }, + expErr: "failed to parse", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + res, err := NewLokiConfig(tc.in) + if tc.expErr != "" { + require.ErrorContains(t, err, tc.expErr) + } else { + require.Equal(t, tc.expRead, res.ReadPathURL.String()) + require.Equal(t, tc.expWrite, res.WritePathURL.String()) + } + }) + } + }) + + t.Run("captures external labels", func(t *testing.T) { + set := setting.UnifiedAlertingLokiSettings{ + LokiRemoteURL: "http://url.com", + ExternalLabels: map[string]string{"a": "b"}, + } + + res, err := NewLokiConfig(set) + + require.NoError(t, err) + require.Contains(t, res.ExternalLabels, "a") + }) +} diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index e794460fc5e..3898ddd0213 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -7,8 +7,10 @@ import ( "time" "github.com/benbjohnson/clock" + "github.com/grafana/alerting/lokiclient" + notificationhistorian "github.com/grafana/alerting/notificationhistorian" "github.com/grafana/alerting/notify/nfstatus" - "github.com/grafana/grafana/pkg/services/ngalert/lokiclient" + "github.com/grafana/grafana/pkg/services/ngalert/lokiconfig" "github.com/prometheus/alertmanager/featurecontrol" "github.com/prometheus/alertmanager/matchers/compat" "golang.org/x/sync/errgroup" @@ -693,7 +695,7 @@ func configureHistorianBackend( return historian.NewAnnotationBackend(annotationBackendLogger, store, rs, met, ac), nil } if backend == historian.BackendTypeLoki { - lcfg, err := lokiclient.NewLokiConfig(cfg.LokiSettings) + lcfg, err := lokiconfig.NewLokiConfig(cfg.LokiSettings) if err != nil { return nil, fmt.Errorf("invalid remote loki configuration: %w", err) } @@ -746,13 +748,13 @@ func configureNotificationHistorian( } met.Info.Set(1) - lcfg, err := lokiclient.NewLokiConfig(cfg.LokiSettings) + lcfg, err := lokiconfig.NewLokiConfig(cfg.LokiSettings) if err != nil { return nil, fmt.Errorf("invalid remote loki configuration: %w", err) } req := lokiclient.NewRequester() logger := log.New("ngalert.notifier.historian").FromContext(ctx) - notificationHistorian := notifier.NewNotificationHistorian(logger, lcfg, req, met, tracer) + notificationHistorian := notificationhistorian.NewNotificationHistorian(logger, lcfg, req, met.BytesWritten, met.WriteDuration, met.WritesTotal, met.WritesFailed, tracer) testConnCtx, cancelFunc := context.WithTimeout(ctx, 10*time.Second) defer cancelFunc() diff --git a/pkg/services/ngalert/notifier/historian.go b/pkg/services/ngalert/notifier/historian.go deleted file mode 100644 index f56175de20c..00000000000 --- a/pkg/services/ngalert/notifier/historian.go +++ /dev/null @@ -1,190 +0,0 @@ -package notifier - -import ( - "context" - "encoding/json" - "fmt" - "strings" - "time" - - alertingModels "github.com/grafana/alerting/models" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/ngalert/client" - "github.com/grafana/grafana/pkg/services/ngalert/lokiclient" - "github.com/grafana/grafana/pkg/services/ngalert/metrics" - "github.com/prometheus/alertmanager/notify" - "github.com/prometheus/alertmanager/types" - prometheusModel "github.com/prometheus/common/model" - "go.opentelemetry.io/otel/trace" -) - -const LokiClientSpanName = "ngalert.notification-historian.client" -const NotificationHistoryWriteTimeout = time.Minute -const NotificationHistoryKey = "from" -const NotificationHistoryLabelValue = "notify-history" - -type NotificationHistoryLokiEntry struct { - SchemaVersion int `json:"schemaVersion"` - Receiver string `json:"receiver"` - Status string `json:"status"` - GroupLabels map[string]string `json:"groupLabels"` - Alerts []NotificationHistoryLokiEntryAlert `json:"alerts"` - Retry bool `json:"retry"` - Error string `json:"error,omitempty"` - Duration int64 `json:"duration"` -} - -type NotificationHistoryLokiEntryAlert struct { - Status string `json:"status"` - Labels map[string]string `json:"labels"` - Annotations map[string]string `json:"annotations"` - StartsAt time.Time `json:"startsAt"` - EndsAt time.Time `json:"endsAt"` - RuleUID string `json:"ruleUID"` -} - -type remoteLokiClient interface { - Ping(context.Context) error - Push(context.Context, []lokiclient.Stream) error -} - -type NotificationHistorian struct { - client remoteLokiClient - externalLabels map[string]string - metrics *metrics.NotificationHistorian - log log.Logger -} - -func NewNotificationHistorian(logger log.Logger, cfg lokiclient.LokiConfig, req client.Requester, metrics *metrics.NotificationHistorian, tracer tracing.Tracer) *NotificationHistorian { - return &NotificationHistorian{ - client: lokiclient.NewLokiClient(cfg, req, metrics.BytesWritten, metrics.WriteDuration, logger, tracer, LokiClientSpanName), - externalLabels: cfg.ExternalLabels, - metrics: metrics, - log: logger, - } -} - -func (h *NotificationHistorian) TestConnection(ctx context.Context) error { - return h.client.Ping(ctx) -} - -func (h *NotificationHistorian) Record(ctx context.Context, alerts []*types.Alert, retry bool, notificationErr error, duration time.Duration) <-chan error { - stream, err := h.prepareStream(ctx, alerts, retry, notificationErr, duration) - logger := h.log.FromContext(ctx) - errCh := make(chan error, 1) - if err != nil { - logger.Error("Failed to convert notification history to stream", "error", err) - errCh <- fmt.Errorf("failed to convert notification history to stream: %w", err) - close(errCh) - return errCh - } - - // This is a new background job, so let's create a new context for it. - // We want it to be isolated, i.e. we don't want grafana shutdowns to interrupt this work - // immediately but rather try to flush writes. - // This also prevents timeouts or other lingering objects (like transactions) from being - // incorrectly propagated here from other areas. - writeCtx := context.Background() - writeCtx, cancel := context.WithTimeout(writeCtx, NotificationHistoryWriteTimeout) - writeCtx = trace.ContextWithSpan(writeCtx, trace.SpanFromContext(ctx)) - - go func(ctx context.Context) { - defer cancel() - defer close(errCh) - logger := h.log.FromContext(ctx) - logger.Debug("Saving notification history") - h.metrics.WritesTotal.Inc() - - if err := h.recordStream(ctx, stream, logger); err != nil { - logger.Error("Failed to save notification history", "error", err) - h.metrics.WritesFailed.Inc() - errCh <- fmt.Errorf("failed to save notification history: %w", err) - } - }(writeCtx) - return errCh -} - -func (h *NotificationHistorian) prepareStream(ctx context.Context, alerts []*types.Alert, retry bool, notificationErr error, duration time.Duration) (lokiclient.Stream, error) { - receiverName, ok := notify.ReceiverName(ctx) - if !ok { - return lokiclient.Stream{}, fmt.Errorf("receiver name not found in context") - } - groupLabels, ok := notify.GroupLabels(ctx) - if !ok { - return lokiclient.Stream{}, fmt.Errorf("group labels not found in context") - } - now, ok := notify.Now(ctx) - if !ok { - return lokiclient.Stream{}, fmt.Errorf("now not found in context") - } - - entryAlerts := make([]NotificationHistoryLokiEntryAlert, len(alerts)) - for i, alert := range alerts { - labels := prepareLabels(alert.Labels) - annotations := prepareLabels(alert.Annotations) - entryAlerts[i] = NotificationHistoryLokiEntryAlert{ - Labels: labels, - Annotations: annotations, - Status: string(alert.StatusAt(now)), - StartsAt: alert.StartsAt, - EndsAt: alert.EndsAt, - RuleUID: string(alert.Labels[alertingModels.RuleUIDLabel]), - } - } - - notificationErrStr := "" - if notificationErr != nil { - notificationErrStr = notificationErr.Error() - } - - entry := NotificationHistoryLokiEntry{ - SchemaVersion: 1, - Receiver: receiverName, - Status: string(types.Alerts(alerts...).StatusAt(now)), - GroupLabels: prepareLabels(groupLabels), - Alerts: entryAlerts, - Retry: retry, - Error: notificationErrStr, - Duration: duration.Milliseconds(), - } - - entryJSON, err := json.Marshal(entry) - if err != nil { - return lokiclient.Stream{}, err - } - - streamLabels := make(map[string]string) - streamLabels[NotificationHistoryKey] = NotificationHistoryLabelValue - for k, v := range h.externalLabels { - streamLabels[k] = v - } - - return lokiclient.Stream{ - Stream: streamLabels, - Values: []lokiclient.Sample{ - { - T: now, - V: string(entryJSON), - }}, - }, nil -} - -func (h *NotificationHistorian) recordStream(ctx context.Context, stream lokiclient.Stream, logger log.Logger) error { - if err := h.client.Push(ctx, []lokiclient.Stream{stream}); err != nil { - return err - } - logger.Debug("Done saving notification history") - return nil -} - -func prepareLabels(labels prometheusModel.LabelSet) map[string]string { - result := make(map[string]string) - for k, v := range labels { - // Remove private labels - if !strings.HasPrefix(string(k), "__") && !strings.HasSuffix(string(k), "__") { - result[string(k)] = string(v) - } - } - return result -} diff --git a/pkg/services/ngalert/notifier/historian_test.go b/pkg/services/ngalert/notifier/historian_test.go deleted file mode 100644 index 92056499dc3..00000000000 --- a/pkg/services/ngalert/notifier/historian_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package notifier - -import ( - "bytes" - "context" - "errors" - "io" - "net/url" - "testing" - "time" - - alertingModels "github.com/grafana/alerting/models" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/ngalert/client" - "github.com/grafana/grafana/pkg/services/ngalert/lokiclient" - "github.com/grafana/grafana/pkg/services/ngalert/metrics" - "github.com/prometheus/alertmanager/notify" - "github.com/prometheus/alertmanager/types" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/testutil" - "github.com/prometheus/common/model" - "github.com/stretchr/testify/require" -) - -var testNow = time.Date(2025, time.July, 15, 16, 55, 0, 0, time.UTC) -var testAlerts = []*types.Alert{ - { - Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "Alert1", alertingModels.RuleUIDLabel: "testRuleUID"}, - Annotations: model.LabelSet{"foo": "bar", "__private__": "baz"}, - StartsAt: testNow, - EndsAt: testNow, - GeneratorURL: "http://localhost/test", - }, - }, -} - -func TestRecord(t *testing.T) { - t.Run("write notification history to Loki", func(t *testing.T) { - testCases := []struct { - name string - retry bool - notificationErr error - expected string - }{ - { - "successful notification", - false, - nil, - "{\"streams\":[{\"stream\":{\"externalLabelKey\":\"externalLabelValue\",\"from\":\"notify-history\"},\"values\":[[\"1752598500000000000\",\"{\\\"schemaVersion\\\":1,\\\"receiver\\\":\\\"testReceiverName\\\",\\\"status\\\":\\\"resolved\\\",\\\"groupLabels\\\":{\\\"foo\\\":\\\"bar\\\"},\\\"alerts\\\":[{\\\"status\\\":\\\"resolved\\\",\\\"labels\\\":{\\\"alertname\\\":\\\"Alert1\\\"},\\\"annotations\\\":{\\\"foo\\\":\\\"bar\\\"},\\\"startsAt\\\":\\\"2025-07-15T16:55:00Z\\\",\\\"endsAt\\\":\\\"2025-07-15T16:55:00Z\\\",\\\"ruleUID\\\":\\\"testRuleUID\\\"}],\\\"retry\\\":false,\\\"duration\\\":1000}\"]]}]}", - }, - { - "failed notification", - true, - errors.New("test notification error"), - "{\"streams\":[{\"stream\":{\"externalLabelKey\":\"externalLabelValue\",\"from\":\"notify-history\"},\"values\":[[\"1752598500000000000\",\"{\\\"schemaVersion\\\":1,\\\"receiver\\\":\\\"testReceiverName\\\",\\\"status\\\":\\\"resolved\\\",\\\"groupLabels\\\":{\\\"foo\\\":\\\"bar\\\"},\\\"alerts\\\":[{\\\"status\\\":\\\"resolved\\\",\\\"labels\\\":{\\\"alertname\\\":\\\"Alert1\\\"},\\\"annotations\\\":{\\\"foo\\\":\\\"bar\\\"},\\\"startsAt\\\":\\\"2025-07-15T16:55:00Z\\\",\\\"endsAt\\\":\\\"2025-07-15T16:55:00Z\\\",\\\"ruleUID\\\":\\\"testRuleUID\\\"}],\\\"retry\\\":true,\\\"error\\\":\\\"test notification error\\\",\\\"duration\\\":1000}\"]]}]}", - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - req := lokiclient.NewFakeRequester() - met := metrics.NewNotificationHistorianMetrics(prometheus.NewRegistry()) - h := createTestNotificationHistorian(req, met) - - err := <-h.Record(recordCtx(), testAlerts, tc.retry, tc.notificationErr, time.Second) - require.NoError(t, err) - - reqBody, err := io.ReadAll(req.LastRequest.Body) - require.NoError(t, err) - require.Equal(t, tc.expected, string(reqBody)) - }) - } - }) - - t.Run("emits expected write metrics", func(t *testing.T) { - reg := prometheus.NewRegistry() - met := metrics.NewNotificationHistorianMetrics(reg) - goodHistorian := createTestNotificationHistorian(lokiclient.NewFakeRequester(), met) - badHistorian := createTestNotificationHistorian(lokiclient.NewFakeRequester().WithResponse(lokiclient.BadResponse()), met) - - <-goodHistorian.Record(recordCtx(), testAlerts, false, nil, time.Second) - <-badHistorian.Record(recordCtx(), testAlerts, false, nil, time.Second) - - exp := bytes.NewBufferString(` -# HELP grafana_alerting_notification_history_writes_failed_total The total number of failed writes of notification history batches. -# TYPE grafana_alerting_notification_history_writes_failed_total counter -grafana_alerting_notification_history_writes_failed_total 1 -# HELP grafana_alerting_notification_history_writes_total The total number of notification history batches that were attempted to be written. -# TYPE grafana_alerting_notification_history_writes_total counter -grafana_alerting_notification_history_writes_total 2 -`) - err := testutil.GatherAndCompare(reg, exp, - "grafana_alerting_notification_history_writes_total", - "grafana_alerting_notification_history_writes_failed_total", - ) - require.NoError(t, err) - }) - - t.Run("returns error when context is missing required fields", func(t *testing.T) { - req := lokiclient.NewFakeRequester() - met := metrics.NewNotificationHistorianMetrics(prometheus.NewRegistry()) - h := createTestNotificationHistorian(req, met) - - err := <-h.Record(context.Background(), testAlerts, false, nil, time.Second) - require.Error(t, err) - }) -} - -func createTestNotificationHistorian(req client.Requester, met *metrics.NotificationHistorian) *NotificationHistorian { - writePathURL, _ := url.Parse("http://some.url") - cfg := lokiclient.LokiConfig{ - WritePathURL: writePathURL, - ExternalLabels: map[string]string{"externalLabelKey": "externalLabelValue"}, - Encoder: lokiclient.JsonEncoder{}, - } - tracer := tracing.InitializeTracerForTest() - return NewNotificationHistorian(log.NewNopLogger(), cfg, req, met, tracer) -} - -func recordCtx() context.Context { - ctx := notify.WithReceiverName(context.Background(), "testReceiverName") - ctx = notify.WithGroupLabels(ctx, model.LabelSet{"foo": "bar"}) - ctx = notify.WithNow(ctx, testNow) - return ctx -} diff --git a/pkg/services/ngalert/remote/client/alertmanager.go b/pkg/services/ngalert/remote/client/alertmanager.go index 52739aab594..7b59f71d8ee 100644 --- a/pkg/services/ngalert/remote/client/alertmanager.go +++ b/pkg/services/ngalert/remote/client/alertmanager.go @@ -8,9 +8,9 @@ import ( "time" httptransport "github.com/go-openapi/runtime/client" + "github.com/grafana/alerting/client" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/ngalert/client" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/util/httpclient" amclient "github.com/prometheus/alertmanager/api/v2/client" diff --git a/pkg/services/ngalert/remote/client/mimir.go b/pkg/services/ngalert/remote/client/mimir.go index d8f9a51f327..931a26fb491 100644 --- a/pkg/services/ngalert/remote/client/mimir.go +++ b/pkg/services/ngalert/remote/client/mimir.go @@ -14,10 +14,10 @@ import ( alertingNotify "github.com/grafana/alerting/notify" + "github.com/grafana/alerting/client" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" - "github.com/grafana/grafana/pkg/services/ngalert/client" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/util/httpclient" ) diff --git a/pkg/services/ngalert/state/historian/loki.go b/pkg/services/ngalert/state/historian/loki.go index c8e9b356552..3c586f4cb2f 100644 --- a/pkg/services/ngalert/state/historian/loki.go +++ b/pkg/services/ngalert/state/historian/loki.go @@ -12,16 +12,16 @@ import ( "time" "github.com/benbjohnson/clock" + "github.com/grafana/alerting/lokiclient" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/services/ngalert/lokiclient" "go.opentelemetry.io/otel/trace" + "github.com/grafana/alerting/client" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" - "github.com/grafana/grafana/pkg/services/ngalert/client" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/models" diff --git a/pkg/services/ngalert/state/historian/loki_test.go b/pkg/services/ngalert/state/historian/loki_test.go index e206fac1df4..fd9fe343f86 100644 --- a/pkg/services/ngalert/state/historian/loki_test.go +++ b/pkg/services/ngalert/state/historian/loki_test.go @@ -13,13 +13,14 @@ import ( "testing" "time" + "github.com/grafana/alerting/lokiclient" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/services/ngalert/lokiclient" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/alerting/client" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -27,7 +28,6 @@ import ( "github.com/grafana/grafana/pkg/services/folder" rulesAuthz "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" acfakes "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes" - "github.com/grafana/grafana/pkg/services/ngalert/client" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/models"