diff --git a/Makefile b/Makefile index 3ee8aa4..85c179c 100644 --- a/Makefile +++ b/Makefile @@ -86,6 +86,10 @@ deps: .protoc-plugins .install-tools .PHONY: mock mock: + PATH="$(LOCAL_BIN):$(PATH)" mockgen \ + -destination=internal/pkg/service/errorgroups/mock/service.go \ + github.com/ozontech/seq-ui/internal/pkg/service/errorgroups \ + Service PATH="$(LOCAL_BIN):$(PATH)" mockgen \ -source=internal/pkg/repository/repository.go \ -destination=internal/pkg/repository/mock/repository.go diff --git a/api/errorgroups/v1/errorgroups.proto b/api/errorgroups/v1/errorgroups.proto index 97e740f..6868c30 100644 --- a/api/errorgroups/v1/errorgroups.proto +++ b/api/errorgroups/v1/errorgroups.proto @@ -9,6 +9,7 @@ option go_package = "github.com/ozontech/seq-ui/pkg/errorgroups/v1;errorgroups"; service ErrorGroupsService { rpc GetGroups(GetGroupsRequest) returns (GetGroupsResponse) {} + rpc GetTopGroups(GetTopGroupsRequest) returns (GetTopGroupsResponse) {} rpc GetHist(GetHistRequest) returns (GetHistResponse) {} rpc GetDetails(GetDetailsRequest) returns (GetDetailsResponse) {} rpc GetReleases(GetReleasesRequest) returns (GetReleasesResponse) {} @@ -40,21 +41,42 @@ message GetGroupsRequest { } message GetGroupsResponse { + message Group { + uint64 hash = 1; + string message = 2; + uint64 seen_total = 3; + google.protobuf.Timestamp first_seen_at = 4; + google.protobuf.Timestamp last_seen_at = 5; + string source = 6; + } + uint64 total = 1; repeated Group groups = 2; } -message Group { - uint64 hash = 1; - string message = 2; - uint64 seen_total = 3; - google.protobuf.Timestamp first_seen_at = 4; - google.protobuf.Timestamp last_seen_at = 5; - string source = 6; +message GetTopGroupsRequest { + optional string env = 1; + optional string source = 2; + google.protobuf.Duration duration = 3; + uint32 limit = 4; + uint32 offset = 5; + bool with_total = 6; +} + +message GetTopGroupsResponse { + message Group { + uint64 hash = 1; + string message = 2; + string source = 3; + uint64 seen_total = 4; + } + + uint64 total = 1; + repeated Group groups = 2; } message GetHistRequest { - string service = 1; + optional string service = 1; optional uint64 group_hash = 2; optional string env = 3; optional string release = 4; @@ -72,7 +94,7 @@ message Bucket { } message GetDetailsRequest { - string service = 1; + optional string service = 1; uint64 group_hash = 2; optional string env = 3; optional string release = 4; @@ -88,6 +110,8 @@ message GetDetailsResponse { message Distributions { repeated Distribution by_env = 1; repeated Distribution by_release = 2; + repeated Distribution by_source = 3; + repeated Distribution by_service = 4; } uint64 group_hash = 1; diff --git a/go.mod b/go.mod index 27a75c8..8e798ca 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ replace github.com/ozontech/seq-ui/pkg => ./pkg require ( github.com/ClickHouse/clickhouse-go/v2 v2.29.0 - github.com/Masterminds/squirrel v1.5.4 github.com/aws/aws-sdk-go v1.55.5 github.com/caarlos0/env/v11 v11.3.1 github.com/cenkalti/backoff/v4 v4.3.0 @@ -18,22 +17,23 @@ require ( github.com/gofrs/uuid v4.4.0+incompatible github.com/golang-jwt/jwt/v4 v4.5.0 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 - github.com/jackc/pgx/v5 v5.7.1 + github.com/jackc/pgx/v5 v5.7.6 github.com/joho/godotenv v1.5.1 github.com/json-iterator/go v1.1.12 + github.com/n-r-w/squirrel v1.5.1 github.com/ozontech/seq-ui/pkg v0.2.0 github.com/prometheus/client_golang v1.20.4 github.com/rakyll/statik v0.1.7 github.com/redis/go-redis/v9 v9.6.1 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.11.1 github.com/throttled/throttled/v2 v2.12.0 - go.opentelemetry.io/otel v1.30.0 + go.opentelemetry.io/otel v1.37.0 go.opentelemetry.io/otel/exporters/jaeger v1.17.0 go.opentelemetry.io/otel/sdk v1.30.0 - go.opentelemetry.io/otel/trace v1.30.0 - go.uber.org/mock v0.5.0 + go.opentelemetry.io/otel/trace v1.37.0 + go.uber.org/mock v0.6.0 go.uber.org/zap v1.27.0 - golang.org/x/sync v0.8.0 + golang.org/x/sync v0.16.0 google.golang.org/grpc v1.67.0 google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v3 v3.0.1 @@ -61,11 +61,11 @@ require ( github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.27.0 // indirect - golang.org/x/net v0.28.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect google.golang.org/genproto v0.0.0-20220908141613-51c1cc9bc6d0 // indirect ) @@ -76,11 +76,11 @@ require ( github.com/go-faster/city v1.0.1 // indirect github.com/go-faster/errors v0.7.1 // indirect github.com/go-jose/go-jose/v4 v4.0.2 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -88,5 +88,7 @@ require ( github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect - go.opentelemetry.io/otel/metric v1.30.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect ) diff --git a/go.sum b/go.sum index ab0376b..b8aa238 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,6 @@ github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeE github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg= github.com/ClickHouse/clickhouse-go/v2 v2.29.0 h1:Dj1w59RssRyLgGHXtYaWU0eIM1pJsu9nGPi/btmvAqw= github.com/ClickHouse/clickhouse-go/v2 v2.29.0/go.mod h1:bLookq6qZJ4Ush/6tOAnJGh1Sf3Sa/nQoMn71p7ZCUE= -github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= -github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= @@ -62,8 +60,8 @@ github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiK github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= @@ -104,8 +102,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -118,8 +116,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= -github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -135,8 +133,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -161,6 +159,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/n-r-w/squirrel v1.5.1 h1:Zvra6guQpPrjlkU9Beo2SGZU/gjxdMiff1sUOhNA62A= +github.com/n-r-w/squirrel v1.5.1/go.mod h1:oAb4J7EGGQ2fUY+kQ3VkLNldLxTNql/xnQDjQI5933s= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= @@ -193,8 +193,8 @@ github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Ung github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= @@ -209,8 +209,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/throttled/throttled/v2 v2.12.0 h1:IezKE1uHlYC/0Al05oZV6Ar+uN/znw3cy9J8banxhEY= github.com/throttled/throttled/v2 v2.12.0/go.mod h1:+EAvrG2hZAQTx8oMpBu8fq6Xmm+d1P2luKK7fIY1Esc= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= @@ -222,23 +222,25 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v0.14.0/go.mod h1:vH5xEuwy7Rts0GNtsCW3HYQoZDY+OmBJ6t1bFGGlxgw= -go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= -go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= -go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= -go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= -go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= -go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -250,9 +252,11 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -274,8 +278,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= @@ -286,8 +290,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -305,8 +309,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -315,8 +319,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/internal/api/errorgroups/v1/errorgroups.go b/internal/api/errorgroups/v1/errorgroups.go index c32ee43..dd3f341 100644 --- a/internal/api/errorgroups/v1/errorgroups.go +++ b/internal/api/errorgroups/v1/errorgroups.go @@ -13,10 +13,10 @@ type ErrorGroups struct { httpAPI *http_api.API } -func New(service *errorgroups.Service) *ErrorGroups { +func New(svc errorgroups.Service) *ErrorGroups { return &ErrorGroups{ - grpcAPI: grpc_api.New(service), - httpAPI: http_api.New(service), + grpcAPI: grpc_api.New(svc), + httpAPI: http_api.New(svc), } } diff --git a/internal/api/errorgroups/v1/grpc/api.go b/internal/api/errorgroups/v1/grpc/api.go index 8f35975..0e057fc 100644 --- a/internal/api/errorgroups/v1/grpc/api.go +++ b/internal/api/errorgroups/v1/grpc/api.go @@ -8,10 +8,10 @@ import ( type API struct { generated.UnimplementedErrorGroupsServiceServer - service *errorgroups.Service + service errorgroups.Service } -func New(svc *errorgroups.Service) *API { +func New(svc errorgroups.Service) *API { return &API{ service: svc, } diff --git a/internal/api/errorgroups/v1/grpc/diff_by_releases_test.go b/internal/api/errorgroups/v1/grpc/diff_by_releases_test.go new file mode 100644 index 0000000..cc6de9f --- /dev/null +++ b/internal/api/errorgroups/v1/grpc/diff_by_releases_test.go @@ -0,0 +1,168 @@ +package grpc + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/ozontech/seq-ui/internal/app/types" + svc_mock "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups/mock" + errorgroups_v1 "github.com/ozontech/seq-ui/pkg/errorgroups/v1" +) + +func TestDiffByReleases(t *testing.T) { + var ( + service = "test-service" + env = "test-env" + source = "test-source" + releases = []string{"release1", "release2"} + now = time.Now() + oneMinuteAgo = now.Add(-1 * time.Minute) + twoMinutesAgo = now.Add(-2 * time.Minute) + someErr = errors.New("some err") + ) + + type mockArgs struct { + req types.DiffByReleasesRequest + + groups []types.DiffGroup + total uint64 + err error + } + + tests := []struct { + name string + + req *errorgroups_v1.DiffByReleasesRequest + want *errorgroups_v1.DiffByReleasesResponse + wantErr bool + + mockArgs *mockArgs + }{ + { + name: "ok", + + req: &errorgroups_v1.DiffByReleasesRequest{ + Service: service, + Releases: releases, + Env: &env, + Source: &source, + Limit: 2, + Offset: 0, + Order: errorgroups_v1.Order_ORDER_LATEST, + WithTotal: true, + }, + want: &errorgroups_v1.DiffByReleasesResponse{ + Total: 10, + Groups: []*errorgroups_v1.DiffByReleasesResponse_Group{ + { + Hash: 123, + Message: "some error 1", + Source: source, + FirstSeenAt: timestamppb.New(twoMinutesAgo), + LastSeenAt: timestamppb.New(oneMinuteAgo), + ReleaseInfos: map[string]*errorgroups_v1.DiffByReleasesResponse_ReleaseInfo{ + "release1": {SeenTotal: 10}, + "release2": {SeenTotal: 20}, + }, + }, + { + Hash: 456, + Message: "some error 2", + Source: source, + FirstSeenAt: timestamppb.New(twoMinutesAgo), + LastSeenAt: timestamppb.New(oneMinuteAgo), + ReleaseInfos: map[string]*errorgroups_v1.DiffByReleasesResponse_ReleaseInfo{ + "release1": {SeenTotal: 30}, + "release2": {SeenTotal: 0}, + }, + }, + }, + }, + + mockArgs: &mockArgs{ + req: types.DiffByReleasesRequest{ + Service: service, + Releases: releases, + Env: &env, + Source: &source, + Limit: 2, + Offset: 0, + Order: types.OrderLatest, + WithTotal: true, + }, + + groups: []types.DiffGroup{ + { + Hash: 123, + Message: "some error 1", + Source: source, + FirstSeenAt: twoMinutesAgo, + LastSeenAt: oneMinuteAgo, + ReleaseInfos: map[string]types.DiffReleaseInfo{ + "release1": {SeenTotal: 10}, + "release2": {SeenTotal: 20}, + }, + }, + { + Hash: 456, + Message: "some error 2", + Source: source, + FirstSeenAt: twoMinutesAgo, + LastSeenAt: oneMinuteAgo, + ReleaseInfos: map[string]types.DiffReleaseInfo{ + "release1": {SeenTotal: 30}, + "release2": {SeenTotal: 0}, + }, + }, + }, + total: 10, + }, + }, + { + name: "err_svc", + + req: &errorgroups_v1.DiffByReleasesRequest{}, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.DiffByReleasesRequest{}, + + err: someErr, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockedSvc := svc_mock.NewMockService(ctrl) + + api := New(mockedSvc) + + if ma := tt.mockArgs; ma != nil { + mockedSvc.EXPECT(). + DiffByReleases(gomock.Any(), ma.req). + Return(ma.groups, ma.total, ma.err). + Times(1) + } + + got, err := api.DiffByReleases(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { + return + } + + require.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/api/errorgroups/v1/grpc/get_details.go b/internal/api/errorgroups/v1/grpc/get_details.go index 6ab7df6..15dbeb1 100644 --- a/internal/api/errorgroups/v1/grpc/get_details.go +++ b/internal/api/errorgroups/v1/grpc/get_details.go @@ -18,9 +18,11 @@ func (a *API) GetDetails(ctx context.Context, req *errorgroups.GetDetailsRequest defer span.End() attributes := []attribute.KeyValue{ - {Key: "service", Value: attribute.StringValue(req.Service)}, {Key: "group_hash", Value: attribute.StringValue(strconv.FormatUint(req.GroupHash, 10))}, } + if req.Service != nil { + attributes = append(attributes, attribute.KeyValue{Key: "service", Value: attribute.StringValue(*req.Service)}) + } if req.Env != nil { attributes = append(attributes, attribute.KeyValue{Key: "env", Value: attribute.StringValue(*req.Env)}) } @@ -45,7 +47,7 @@ func (a *API) GetDetails(ctx context.Context, req *errorgroups.GetDetailsRequest } return &errorgroups.GetDetailsResponse{ - GroupHash: details.GroupHash, + GroupHash: details.Hash, Message: details.Message, SeenTotal: details.SeenTotal, FirstSeenAt: timestamppb.New(details.FirstSeenAt), @@ -57,24 +59,26 @@ func (a *API) GetDetails(ctx context.Context, req *errorgroups.GetDetailsRequest } func distributionsToProto(source types.ErrorGroupDistributions) *errorgroups.GetDetailsResponse_Distributions { - distrToProto := func(d types.ErrorGroupDistribution) *errorgroups.GetDetailsResponse_Distribution { - return &errorgroups.GetDetailsResponse_Distribution{ - Value: d.Value, - Percent: d.Percent, + distrToProto := func(ds []types.ErrorGroupDistribution) []*errorgroups.GetDetailsResponse_Distribution { + if len(ds) == 0 { + return nil } - } - ds := &errorgroups.GetDetailsResponse_Distributions{ - ByEnv: make([]*errorgroups.GetDetailsResponse_Distribution, 0, len(source.ByEnv)), - ByRelease: make([]*errorgroups.GetDetailsResponse_Distribution, 0, len(source.ByRelease)), - } + res := make([]*errorgroups.GetDetailsResponse_Distribution, 0, len(ds)) + for _, d := range ds { + res = append(res, &errorgroups.GetDetailsResponse_Distribution{ + Value: d.Value, + Percent: d.Percent, + }) + } - for _, d := range source.ByEnv { - ds.ByEnv = append(ds.ByEnv, distrToProto(d)) - } - for _, d := range source.ByRelease { - ds.ByRelease = append(ds.ByRelease, distrToProto(d)) + return res } - return ds + return &errorgroups.GetDetailsResponse_Distributions{ + ByEnv: distrToProto(source.ByEnv), + BySource: distrToProto(source.BySource), + ByService: distrToProto(source.ByService), + ByRelease: distrToProto(source.ByRelease), + } } diff --git a/internal/api/errorgroups/v1/grpc/get_details_test.go b/internal/api/errorgroups/v1/grpc/get_details_test.go index 967cc37..38b7390 100644 --- a/internal/api/errorgroups/v1/grpc/get_details_test.go +++ b/internal/api/errorgroups/v1/grpc/get_details_test.go @@ -2,152 +2,142 @@ package grpc import ( "context" + "errors" "testing" "time" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/ozontech/seq-ui/internal/app/config" "github.com/ozontech/seq-ui/internal/app/types" - repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository_ch/mock" - "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups" + svc_mock "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups/mock" errorgroups_v1 "github.com/ozontech/seq-ui/pkg/errorgroups/v1" ) func TestGetDetails(t *testing.T) { var ( - service = "service1" - groupHash uint64 = 123 - env = "prod" - release = "test_release" - now = time.Now() - oneMinuteAgo = now.Add(-1 * time.Minute) - twoMinutesAgo = now.Add(-2 * time.Minute) + groupHash = uint64(123) + service = "test-service" + env = "test-env" + release = "test-release" + source = "test-source" + msg = "some error" + now = time.Now() + oneMinuteAgo = now.Add(-1 * time.Minute) + twoMinutesAgo = now.Add(-2 * time.Minute) + someErr = errors.New("some err") ) type mockArgs struct { - req types.GetErrorGroupDetailsRequest - detailsResp types.ErrorGroupDetails - countsResp *types.ErrorGroupCounts - err error + req types.GetErrorGroupDetailsRequest + + details types.ErrorGroupDetails + err error } tests := []struct { name string - req *errorgroups_v1.GetDetailsRequest - want *errorgroups_v1.GetDetailsResponse - wantCode codes.Code + req *errorgroups_v1.GetDetailsRequest + want *errorgroups_v1.GetDetailsResponse + wantErr bool mockArgs *mockArgs }{ { - name: "success_all_fields", + name: "ok", + req: &errorgroups_v1.GetDetailsRequest{ - Service: service, GroupHash: groupHash, + Service: &service, Env: &env, Release: &release, + Source: &source, }, want: &errorgroups_v1.GetDetailsResponse{ - GroupHash: 123, - Message: "some error", + GroupHash: groupHash, + Message: msg, SeenTotal: 10, FirstSeenAt: timestamppb.New(twoMinutesAgo), LastSeenAt: timestamppb.New(oneMinuteAgo), - Distributions: &errorgroups_v1.GetDetailsResponse_Distributions{ - ByEnv: []*errorgroups_v1.GetDetailsResponse_Distribution{ - {Value: env, Percent: 100}, - }, - ByRelease: []*errorgroups_v1.GetDetailsResponse_Distribution{ - {Value: release, Percent: 100}, - }, + Source: source, + LogTags: map[string]string{ + "tag1": "val1", + "tag2": "val2", }, - }, - wantCode: codes.OK, - mockArgs: &mockArgs{ - req: types.GetErrorGroupDetailsRequest{ - Service: service, - GroupHash: groupHash, - Env: &env, - Release: &release, - }, - detailsResp: types.ErrorGroupDetails{ - GroupHash: 123, - Message: "some error", - SeenTotal: 10, - FirstSeenAt: twoMinutesAgo, - LastSeenAt: oneMinuteAgo, - }, - }, - }, - { - name: "success_only_required_fields", - req: &errorgroups_v1.GetDetailsRequest{ - Service: service, - GroupHash: groupHash, - }, - want: &errorgroups_v1.GetDetailsResponse{ - GroupHash: 123, - Message: "some error", - SeenTotal: 10, - FirstSeenAt: timestamppb.New(twoMinutesAgo), - LastSeenAt: timestamppb.New(oneMinuteAgo), Distributions: &errorgroups_v1.GetDetailsResponse_Distributions{ ByEnv: []*errorgroups_v1.GetDetailsResponse_Distribution{ {Value: "env1", Percent: 70}, {Value: "env2", Percent: 30}, }, + BySource: []*errorgroups_v1.GetDetailsResponse_Distribution{ + {Value: "source1", Percent: 50}, + {Value: "source2", Percent: 50}, + }, + ByService: []*errorgroups_v1.GetDetailsResponse_Distribution{ + {Value: "service1", Percent: 100}, + {Value: "service2", Percent: 0}, + }, ByRelease: []*errorgroups_v1.GetDetailsResponse_Distribution{ - {Value: "release2", Percent: 60}, - {Value: "release1", Percent: 40}, + {Value: "release1", Percent: 60}, + {Value: "release2", Percent: 40}, }, }, }, - wantCode: codes.OK, + mockArgs: &mockArgs{ req: types.GetErrorGroupDetailsRequest{ - Service: service, + Service: &service, GroupHash: groupHash, + Env: &env, + Release: &release, + Source: &source, }, - detailsResp: types.ErrorGroupDetails{ - GroupHash: 123, - Message: "some error", + + details: types.ErrorGroupDetails{ + Hash: groupHash, + Message: msg, SeenTotal: 10, FirstSeenAt: twoMinutesAgo, LastSeenAt: oneMinuteAgo, - }, - countsResp: &types.ErrorGroupCounts{ - ByEnv: types.ErrorGroupCount{ - "env1": 7, - "env2": 3, + Source: source, + LogTags: map[string]string{ + "tag1": "val1", + "tag2": "val2", }, - ByRelease: types.ErrorGroupCount{ - "release1": 4, - "release2": 6, + Distributions: types.ErrorGroupDistributions{ + ByEnv: []types.ErrorGroupDistribution{ + {Value: "env1", Percent: 70}, + {Value: "env2", Percent: 30}, + }, + BySource: []types.ErrorGroupDistribution{ + {Value: "source1", Percent: 50}, + {Value: "source2", Percent: 50}, + }, + ByService: []types.ErrorGroupDistribution{ + {Value: "service1", Percent: 100}, + {Value: "service2", Percent: 0}, + }, + ByRelease: []types.ErrorGroupDistribution{ + {Value: "release1", Percent: 60}, + {Value: "release2", Percent: 40}, + }, }, }, }, }, { - name: "err_no_service_field", - req: &errorgroups_v1.GetDetailsRequest{ - GroupHash: groupHash, - Env: &env, - }, - wantCode: codes.InvalidArgument, - }, - { - name: "err_no_group_hash_field", - req: &errorgroups_v1.GetDetailsRequest{ - Service: service, - Env: &env, + name: "err_svc", + + req: &errorgroups_v1.GetDetailsRequest{}, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupDetailsRequest{}, + + err: someErr, }, - wantCode: codes.InvalidArgument, }, } @@ -157,25 +147,24 @@ func TestGetDetails(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - mockedRepo := repo_mock.NewMockRepository(ctrl) - api := New(errorgroups.New(mockedRepo, config.LogTagsMapping{})) - - if tt.mockArgs != nil { - mockedRepo.EXPECT().GetErrorDetails(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.detailsResp, tt.mockArgs.err).Times(1) - if tt.mockArgs.countsResp != nil { - mockedRepo.EXPECT().GetErrorCounts(gomock.Any(), tt.mockArgs.req). - Return(*tt.mockArgs.countsResp, tt.mockArgs.err).Times(1) - } + mockedSvc := svc_mock.NewMockService(ctrl) + + api := New(mockedSvc) + + if ma := tt.mockArgs; ma != nil { + mockedSvc.EXPECT(). + GetDetails(gomock.Any(), ma.req). + Return(ma.details, ma.err). + Times(1) } - ctx := context.Background() - got, err := api.GetDetails(ctx, tt.req) + got, err := api.GetDetails(context.Background(), tt.req) - require.Equal(t, tt.wantCode, status.Code(err)) - if tt.wantCode != codes.OK { + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { return } + require.Equal(t, tt.want, got) }) } diff --git a/internal/api/errorgroups/v1/grpc/get_groups.go b/internal/api/errorgroups/v1/grpc/get_groups.go index d91ffc4..4a72451 100644 --- a/internal/api/errorgroups/v1/grpc/get_groups.go +++ b/internal/api/errorgroups/v1/grpc/get_groups.go @@ -66,7 +66,6 @@ func (a *API) GetGroups(ctx context.Context, req *errorgroups.GetGroupsRequest) total uint64 err error ) - if req.Filter != nil && req.Filter.IsNew { groups, total, err = a.service.GetNewErrorGroups(ctx, request) } else { @@ -83,14 +82,14 @@ func (a *API) GetGroups(ctx context.Context, req *errorgroups.GetGroupsRequest) }, nil } -func groupsToProto(source []types.ErrorGroup) []*errorgroups.Group { - groups := make([]*errorgroups.Group, 0, len(source)) +func groupsToProto(source []types.ErrorGroup) []*errorgroups.GetGroupsResponse_Group { + groups := make([]*errorgroups.GetGroupsResponse_Group, 0, len(source)) for _, g := range source { - groups = append(groups, &errorgroups.Group{ + groups = append(groups, &errorgroups.GetGroupsResponse_Group{ Hash: g.Hash, Message: g.Message, - SeenTotal: g.SeenTotal, + SeenTotal: g.Count, FirstSeenAt: timestamppb.New(g.FirstSeenAt), LastSeenAt: timestamppb.New(g.LastSeenAt), Source: g.Source, diff --git a/internal/api/errorgroups/v1/grpc/get_groups_test.go b/internal/api/errorgroups/v1/grpc/get_groups_test.go index 31f3c57..a15d5f3 100644 --- a/internal/api/errorgroups/v1/grpc/get_groups_test.go +++ b/internal/api/errorgroups/v1/grpc/get_groups_test.go @@ -2,69 +2,71 @@ package grpc import ( "context" + "errors" "testing" "time" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/ozontech/seq-ui/internal/app/config" "github.com/ozontech/seq-ui/internal/app/types" - repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository_ch/mock" - "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups" + svc_mock "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups/mock" errorgroups_v1 "github.com/ozontech/seq-ui/pkg/errorgroups/v1" ) func TestGetGroups(t *testing.T) { var ( - service = "service1" - env = "prod" - release = "v1" + service = "test-service" + env = "test-env" + source = "test-source" + release = "test-release" duration = 2 * time.Minute now = time.Now() oneMinuteAgo = now.Add(-1 * time.Minute) twoMinutesAgo = now.Add(-2 * time.Minute) + someErr = errors.New("some err") ) type mockArgs struct { - req types.GetErrorGroupsRequest - groupsResp []types.ErrorGroup - countsResp uint64 - err error + req types.GetErrorGroupsRequest + + groups []types.ErrorGroup + total uint64 + err error } tests := []struct { name string - req *errorgroups_v1.GetGroupsRequest - want *errorgroups_v1.GetGroupsResponse - wantCode codes.Code + req *errorgroups_v1.GetGroupsRequest + want *errorgroups_v1.GetGroupsResponse + wantErr bool mockArgs *mockArgs - noUser bool }{ { - name: "success_all_fields", + name: "ok", + req: &errorgroups_v1.GetGroupsRequest{ Service: service, Env: &env, + Source: &source, Release: &release, - Duration: durationpb.New(2 * time.Minute), - Limit: 10, + Duration: durationpb.New(duration), + Limit: 2, Offset: 0, Order: errorgroups_v1.Order_ORDER_OLDEST, WithTotal: true, }, want: &errorgroups_v1.GetGroupsResponse{ - Total: 2, - Groups: []*errorgroups_v1.Group{ + Total: 10, + Groups: []*errorgroups_v1.GetGroupsResponse_Group{ { Hash: 123, Message: "some error 1", + Source: source, SeenTotal: 10, FirstSeenAt: timestamppb.New(twoMinutesAgo), LastSeenAt: timestamppb.New(oneMinuteAgo), @@ -72,53 +74,72 @@ func TestGetGroups(t *testing.T) { { Hash: 456, Message: "some error 2", + Source: source, SeenTotal: 5, FirstSeenAt: timestamppb.New(twoMinutesAgo), LastSeenAt: timestamppb.New(oneMinuteAgo), }, }, }, - wantCode: codes.OK, + mockArgs: &mockArgs{ req: types.GetErrorGroupsRequest{ Service: service, Env: &env, + Source: &source, Release: &release, Duration: &duration, - Limit: 10, + Limit: 2, Offset: 0, Order: types.OrderOldest, WithTotal: true, }, - groupsResp: []types.ErrorGroup{ + + groups: []types.ErrorGroup{ { Hash: 123, Message: "some error 1", - SeenTotal: 10, + Source: source, + Count: 10, FirstSeenAt: twoMinutesAgo, LastSeenAt: oneMinuteAgo, }, { Hash: 456, Message: "some error 2", - SeenTotal: 5, + Source: source, + Count: 5, FirstSeenAt: twoMinutesAgo, LastSeenAt: oneMinuteAgo, }, }, - countsResp: 2, + total: 10, }, }, { - name: "success_only_required_fields", + name: "ok_new", + req: &errorgroups_v1.GetGroupsRequest{ - Service: service, + Service: service, + Env: &env, + Source: &source, + Release: &release, + Duration: durationpb.New(duration), + Limit: 2, + Offset: 0, + Order: errorgroups_v1.Order_ORDER_OLDEST, + WithTotal: true, + Filter: &errorgroups_v1.GetGroupsRequest_Filter{ + IsNew: true, + }, }, want: &errorgroups_v1.GetGroupsResponse{ - Groups: []*errorgroups_v1.Group{ + Total: 10, + Groups: []*errorgroups_v1.GetGroupsResponse_Group{ { Hash: 123, Message: "some error 1", + Source: source, SeenTotal: 10, FirstSeenAt: timestamppb.New(twoMinutesAgo), LastSeenAt: timestamppb.New(oneMinuteAgo), @@ -126,45 +147,74 @@ func TestGetGroups(t *testing.T) { { Hash: 456, Message: "some error 2", + Source: source, SeenTotal: 5, FirstSeenAt: timestamppb.New(twoMinutesAgo), LastSeenAt: timestamppb.New(oneMinuteAgo), }, }, }, - wantCode: codes.OK, + mockArgs: &mockArgs{ req: types.GetErrorGroupsRequest{ - Service: service, - Limit: 25, - Offset: 0, - Order: types.OrderFrequent, + Service: service, + Env: &env, + Source: &source, + Release: &release, + Duration: &duration, + Limit: 2, + Offset: 0, + Order: types.OrderOldest, + WithTotal: true, }, - groupsResp: []types.ErrorGroup{ + + groups: []types.ErrorGroup{ { Hash: 123, Message: "some error 1", - SeenTotal: 10, + Source: source, + Count: 10, FirstSeenAt: twoMinutesAgo, LastSeenAt: oneMinuteAgo, }, { Hash: 456, Message: "some error 2", - SeenTotal: 5, + Source: source, + Count: 5, FirstSeenAt: twoMinutesAgo, LastSeenAt: oneMinuteAgo, }, }, + total: 10, }, }, { - name: "err_no_service_field", + name: "err_svc", + + req: &errorgroups_v1.GetGroupsRequest{}, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupsRequest{}, + + err: someErr, + }, + }, + { + name: "err_svc_new", req: &errorgroups_v1.GetGroupsRequest{ - Env: &env, - Release: &release, + Filter: &errorgroups_v1.GetGroupsRequest_Filter{ + IsNew: true, + }, + }, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupsRequest{}, + + err: someErr, }, - wantCode: codes.InvalidArgument, }, } @@ -174,23 +224,28 @@ func TestGetGroups(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - mockedRepo := repo_mock.NewMockRepository(ctrl) - api := New(errorgroups.New(mockedRepo, config.LogTagsMapping{})) - - if tt.mockArgs != nil { - mockedRepo.EXPECT().GetErrorGroups(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.groupsResp, tt.mockArgs.err).Times(1) - if tt.mockArgs.req.WithTotal { - mockedRepo.EXPECT().GetErrorGroupsCount(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.countsResp, tt.mockArgs.err).Times(1) + mockedSvc := svc_mock.NewMockService(ctrl) + + api := New(mockedSvc) + + if ma := tt.mockArgs; ma != nil { + if tt.req.Filter != nil && tt.req.Filter.IsNew { + mockedSvc.EXPECT(). + GetNewErrorGroups(gomock.Any(), ma.req). + Return(ma.groups, ma.total, ma.err). + Times(1) + } else { + mockedSvc.EXPECT(). + GetErrorGroups(gomock.Any(), ma.req). + Return(ma.groups, ma.total, ma.err). + Times(1) } } - ctx := context.Background() - got, err := api.GetGroups(ctx, tt.req) + got, err := api.GetGroups(context.Background(), tt.req) - require.Equal(t, tt.wantCode, status.Code(err)) - if tt.wantCode != codes.OK { + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { return } diff --git a/internal/api/errorgroups/v1/grpc/get_hist.go b/internal/api/errorgroups/v1/grpc/get_hist.go index cc2b50d..abaf5f7 100644 --- a/internal/api/errorgroups/v1/grpc/get_hist.go +++ b/internal/api/errorgroups/v1/grpc/get_hist.go @@ -18,14 +18,15 @@ func (a *API) GetHist(ctx context.Context, req *errorgroups.GetHistRequest) (*er ctx, span := tracing.StartSpan(ctx, "errorgroups_v1_get_hist") defer span.End() - attributes := []attribute.KeyValue{ - {Key: "service", Value: attribute.StringValue(req.Service)}, - } + attributes := []attribute.KeyValue{} if req.GroupHash != nil { attributes = append(attributes, attribute.KeyValue{ Key: "group_hash", Value: attribute.StringValue(strconv.FormatUint(*req.GroupHash, 10)), }) } + if req.Service != nil { + attributes = append(attributes, attribute.KeyValue{Key: "service", Value: attribute.StringValue(*req.Service)}) + } if req.Env != nil { attributes = append(attributes, attribute.KeyValue{Key: "env", Value: attribute.StringValue(*req.Env)}) } diff --git a/internal/api/errorgroups/v1/grpc/get_hist_test.go b/internal/api/errorgroups/v1/grpc/get_hist_test.go index 010b41c..7893ab4 100644 --- a/internal/api/errorgroups/v1/grpc/get_hist_test.go +++ b/internal/api/errorgroups/v1/grpc/get_hist_test.go @@ -2,59 +2,60 @@ package grpc import ( "context" + "errors" "testing" "time" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/ozontech/seq-ui/internal/app/config" "github.com/ozontech/seq-ui/internal/app/types" - repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository_ch/mock" - "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups" + svc_mock "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups/mock" errorgroups_v1 "github.com/ozontech/seq-ui/pkg/errorgroups/v1" ) func TestGetHist(t *testing.T) { var ( - service = "service1" - groupHash uint64 = 123 - env = "prod" - release = "v1" - twoMinutes = 2 * time.Minute - now = time.Now() - oneMinuteAgo = now.Add(-1 * time.Minute) - twoMinutesAgo = now.Add(-2 * time.Minute) + service = "test-service" + groupHash = uint64(123) + env = "test-env" + source = "test-source" + release = "test-release" + duration = 2 * time.Minute + now = time.Now() + oneMinuteAgo = now.Add(-1 * time.Minute) + twoMinutesAgo = now.Add(-2 * time.Minute) + someErr = errors.New("some err") ) type mockArgs struct { - req types.GetErrorHistRequest - resp []types.ErrorHistBucket - err error + req types.GetErrorHistRequest + + buckets []types.ErrorHistBucket + err error } tests := []struct { name string - req *errorgroups_v1.GetHistRequest - want *errorgroups_v1.GetHistResponse - wantCode codes.Code + req *errorgroups_v1.GetHistRequest + want *errorgroups_v1.GetHistResponse + wantErr bool mockArgs *mockArgs - noUser bool }{ { - name: "success_all_fields", + name: "ok", + req: &errorgroups_v1.GetHistRequest{ - Service: service, GroupHash: &groupHash, + Service: &service, Env: &env, + Source: &source, Release: &release, - Duration: durationpb.New(2 * time.Minute), + Duration: durationpb.New(duration), }, want: &errorgroups_v1.GetHistResponse{ Buckets: []*errorgroups_v1.Bucket{ @@ -62,50 +63,34 @@ func TestGetHist(t *testing.T) { {Time: timestamppb.New(twoMinutesAgo), Count: 20}, }, }, - wantCode: codes.OK, + mockArgs: &mockArgs{ req: types.GetErrorHistRequest{ - Service: service, GroupHash: &groupHash, + Service: &service, Env: &env, + Source: &source, Release: &release, - Duration: &twoMinutes, + Duration: &duration, }, - resp: []types.ErrorHistBucket{ + + buckets: []types.ErrorHistBucket{ {Time: oneMinuteAgo, Count: 10}, {Time: twoMinutesAgo, Count: 20}, }, }, }, { - name: "success_only_service_field", - req: &errorgroups_v1.GetHistRequest{ - Service: service, - }, - want: &errorgroups_v1.GetHistResponse{ - Buckets: []*errorgroups_v1.Bucket{ - {Time: timestamppb.New(oneMinuteAgo), Count: 10}, - {Time: timestamppb.New(twoMinutesAgo), Count: 20}, - }, - }, - wantCode: codes.OK, + name: "err_svc", + + req: &errorgroups_v1.GetHistRequest{}, + wantErr: true, + mockArgs: &mockArgs{ - req: types.GetErrorHistRequest{ - Service: service, - }, - resp: []types.ErrorHistBucket{ - {Time: oneMinuteAgo, Count: 10}, - {Time: twoMinutesAgo, Count: 20}, - }, - }, - }, - { - name: "err_no_service_field", - req: &errorgroups_v1.GetHistRequest{ - GroupHash: &groupHash, - Env: &env, + req: types.GetErrorHistRequest{}, + + err: someErr, }, - wantCode: codes.InvalidArgument, }, } @@ -115,19 +100,21 @@ func TestGetHist(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - mockedRepo := repo_mock.NewMockRepository(ctrl) - api := New(errorgroups.New(mockedRepo, config.LogTagsMapping{})) + mockedSvc := svc_mock.NewMockService(ctrl) + + api := New(mockedSvc) - if tt.mockArgs != nil { - mockedRepo.EXPECT().GetErrorHist(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) + if ma := tt.mockArgs; ma != nil { + mockedSvc.EXPECT(). + GetHist(gomock.Any(), ma.req). + Return(ma.buckets, ma.err). + Times(1) } - ctx := context.Background() - got, err := api.GetHist(ctx, tt.req) + got, err := api.GetHist(context.Background(), tt.req) - require.Equal(t, tt.wantCode, status.Code(err)) - if tt.wantCode != codes.OK { + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { return } diff --git a/internal/api/errorgroups/v1/grpc/get_releases.go b/internal/api/errorgroups/v1/grpc/get_releases.go index 032151c..1a5ff8b 100644 --- a/internal/api/errorgroups/v1/grpc/get_releases.go +++ b/internal/api/errorgroups/v1/grpc/get_releases.go @@ -23,7 +23,7 @@ func (a *API) GetReleases(ctx context.Context, req *errorgroups.GetReleasesReque } span.SetAttributes(attributes...) - request := types.GetErrorGroupReleasesRequest{ + request := types.GetReleasesRequest{ Service: req.Service, Env: req.Env, } diff --git a/internal/api/errorgroups/v1/grpc/get_releases_test.go b/internal/api/errorgroups/v1/grpc/get_releases_test.go index 206786f..fc4706a 100644 --- a/internal/api/errorgroups/v1/grpc/get_releases_test.go +++ b/internal/api/errorgroups/v1/grpc/get_releases_test.go @@ -2,116 +2,70 @@ package grpc import ( "context" + "errors" "testing" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "github.com/ozontech/seq-ui/internal/app/config" "github.com/ozontech/seq-ui/internal/app/types" - repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository_ch/mock" - "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups" + svc_mock "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups/mock" errorgroups_v1 "github.com/ozontech/seq-ui/pkg/errorgroups/v1" ) func TestGetReleases(t *testing.T) { var ( - service = "service1" - env = "prod" + service = "test-service" + env = "test-env" + someErr = errors.New("some err") ) type mockArgs struct { - req types.GetErrorGroupReleasesRequest - resp []string - err error + req types.GetReleasesRequest + + releases []string + err error } tests := []struct { name string - req *errorgroups_v1.GetReleasesRequest - want *errorgroups_v1.GetReleasesResponse - wantCode codes.Code + req *errorgroups_v1.GetReleasesRequest + want *errorgroups_v1.GetReleasesResponse + wantErr bool mockArgs *mockArgs - noUser bool }{ { - name: "success_all_fields", - req: &errorgroups_v1.GetReleasesRequest{ - Service: service, - Env: &env, - }, - want: &errorgroups_v1.GetReleasesResponse{ - Releases: []string{"v1", "v2"}, - }, - wantCode: codes.OK, - mockArgs: &mockArgs{ - req: types.GetErrorGroupReleasesRequest{ - Service: service, - Env: &env, - }, - resp: []string{"v1", "v2"}, - }, - }, - { - name: "success_no_env_field", - req: &errorgroups_v1.GetReleasesRequest{ - Service: service, - }, - want: &errorgroups_v1.GetReleasesResponse{ - Releases: []string{"v1", "v2"}, - }, - wantCode: codes.OK, - mockArgs: &mockArgs{ - req: types.GetErrorGroupReleasesRequest{ - Service: service, - }, - resp: []string{"v1", "v2"}, - }, - }, - { - name: "success_no_group_hash_field", + name: "ok", + req: &errorgroups_v1.GetReleasesRequest{ Service: service, Env: &env, }, want: &errorgroups_v1.GetReleasesResponse{ - Releases: []string{"v1", "v2"}, + Releases: []string{"release1", "release2"}, }, - wantCode: codes.OK, + mockArgs: &mockArgs{ - req: types.GetErrorGroupReleasesRequest{ + req: types.GetReleasesRequest{ Service: service, Env: &env, }, - resp: []string{"v1", "v2"}, + + releases: []string{"release1", "release2"}, }, }, { - name: "success_only_service_field", - req: &errorgroups_v1.GetReleasesRequest{ - Service: service, - }, - want: &errorgroups_v1.GetReleasesResponse{ - Releases: []string{"v1", "v2"}, - }, - wantCode: codes.OK, + name: "err_svc", + req: &errorgroups_v1.GetReleasesRequest{}, + wantErr: true, + mockArgs: &mockArgs{ - req: types.GetErrorGroupReleasesRequest{ - Service: service, - }, - resp: []string{"v1", "v2"}, - }, - }, - { - name: "err_no_service_field", - req: &errorgroups_v1.GetReleasesRequest{ - Env: &env, + req: types.GetReleasesRequest{}, + + err: someErr, }, - wantCode: codes.InvalidArgument, }, } @@ -121,19 +75,21 @@ func TestGetReleases(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - mockedRepo := repo_mock.NewMockRepository(ctrl) - api := New(errorgroups.New(mockedRepo, config.LogTagsMapping{})) + mockedSvc := svc_mock.NewMockService(ctrl) + + api := New(mockedSvc) - if tt.mockArgs != nil { - mockedRepo.EXPECT().GetErrorReleases(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) + if ma := tt.mockArgs; ma != nil { + mockedSvc.EXPECT(). + GetReleases(gomock.Any(), ma.req). + Return(ma.releases, ma.err). + Times(1) } - ctx := context.Background() - got, err := api.GetReleases(ctx, tt.req) + got, err := api.GetReleases(context.Background(), tt.req) - require.Equal(t, tt.wantCode, status.Code(err)) - if tt.wantCode != codes.OK { + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { return } diff --git a/internal/api/errorgroups/v1/grpc/get_services_test.go b/internal/api/errorgroups/v1/grpc/get_services_test.go index 8df4a5a..0595800 100644 --- a/internal/api/errorgroups/v1/grpc/get_services_test.go +++ b/internal/api/errorgroups/v1/grpc/get_services_test.go @@ -2,102 +2,74 @@ package grpc import ( "context" + "errors" "testing" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "github.com/ozontech/seq-ui/internal/app/config" "github.com/ozontech/seq-ui/internal/app/types" - repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository_ch/mock" - "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups" + svc_mock "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups/mock" errorgroups_v1 "github.com/ozontech/seq-ui/pkg/errorgroups/v1" ) func TestGetServices(t *testing.T) { var ( - query = "itc" - env = "prod" + query = "test-query" + env = "test-env" + someErr = errors.New("some err") ) type mockArgs struct { - req types.GetServicesRequest - resp []string - err error + req types.GetServicesRequest + + services []string + err error } tests := []struct { name string - req *errorgroups_v1.GetServicesRequest - want *errorgroups_v1.GetServicesResponse - wantCode codes.Code + req *errorgroups_v1.GetServicesRequest + want *errorgroups_v1.GetServicesResponse + wantErr bool mockArgs *mockArgs - noUser bool }{ { - name: "success_all_fields", - req: &errorgroups_v1.GetServicesRequest{ - Query: query, - Env: &env, - }, - want: &errorgroups_v1.GetServicesResponse{ - Services: []string{"service1", "service2"}, - }, - wantCode: codes.OK, - mockArgs: &mockArgs{ - req: types.GetServicesRequest{ - Query: query, - Env: &env, - }, - resp: []string{"service1", "service2"}, - }, - }, - { - name: "success_no_env_field", - req: &errorgroups_v1.GetServicesRequest{ - Query: query, - }, - want: &errorgroups_v1.GetServicesResponse{ - Services: []string{"service1", "service2"}, - }, - wantCode: codes.OK, - mockArgs: &mockArgs{ - req: types.GetServicesRequest{ - Query: query, - }, - resp: []string{"service1", "service2"}, - }, - }, - { - name: "success_no_query_field", + name: "ok", + req: &errorgroups_v1.GetServicesRequest{ - Env: &env, + Query: query, + Env: &env, + Limit: 10, + Offset: 20, }, want: &errorgroups_v1.GetServicesResponse{ Services: []string{"service1", "service2"}, }, - wantCode: codes.OK, + mockArgs: &mockArgs{ req: types.GetServicesRequest{ - Env: &env, + Query: query, + Env: &env, + Limit: 10, + Offset: 20, }, - resp: []string{"service1", "service2"}, + + services: []string{"service1", "service2"}, }, }, { - name: "success_no_query_no_env", - req: &errorgroups_v1.GetServicesRequest{}, - want: &errorgroups_v1.GetServicesResponse{ - Services: []string{"service1", "service2"}, - }, - wantCode: codes.OK, + name: "err_svc", + + req: &errorgroups_v1.GetServicesRequest{}, + wantErr: true, + mockArgs: &mockArgs{ - req: types.GetServicesRequest{}, - resp: []string{"service1", "service2"}, + req: types.GetServicesRequest{}, + + err: someErr, }, }, } @@ -108,19 +80,21 @@ func TestGetServices(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - mockedRepo := repo_mock.NewMockRepository(ctrl) - api := New(errorgroups.New(mockedRepo, config.LogTagsMapping{})) + mockedSvc := svc_mock.NewMockService(ctrl) + + api := New(mockedSvc) - if tt.mockArgs != nil { - mockedRepo.EXPECT().GetServices(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) + if ma := tt.mockArgs; ma != nil { + mockedSvc.EXPECT(). + GetServices(gomock.Any(), ma.req). + Return(ma.services, ma.err). + Times(1) } - ctx := context.Background() - got, err := api.GetServices(ctx, tt.req) + got, err := api.GetServices(context.Background(), tt.req) - require.Equal(t, tt.wantCode, status.Code(err)) - if tt.wantCode != codes.OK { + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { return } diff --git a/internal/api/errorgroups/v1/grpc/get_top_groups.go b/internal/api/errorgroups/v1/grpc/get_top_groups.go new file mode 100644 index 0000000..3d82aab --- /dev/null +++ b/internal/api/errorgroups/v1/grpc/get_top_groups.go @@ -0,0 +1,73 @@ +package grpc + +import ( + "context" + "time" + + "github.com/ozontech/seq-ui/internal/api/grpcutil" + "github.com/ozontech/seq-ui/internal/app/types" + "github.com/ozontech/seq-ui/pkg/errorgroups/v1" + "github.com/ozontech/seq-ui/tracing" + "go.opentelemetry.io/otel/attribute" +) + +func (a *API) GetTopGroups(ctx context.Context, req *errorgroups.GetTopGroupsRequest) (*errorgroups.GetTopGroupsResponse, error) { + ctx, span := tracing.StartSpan(ctx, "errorgroups_v1_get_top_groups") + defer span.End() + + attributes := []attribute.KeyValue{ + {Key: "limit", Value: attribute.IntValue(int(req.Limit))}, + {Key: "offset", Value: attribute.IntValue(int(req.Offset))}, + {Key: "with_total", Value: attribute.BoolValue(req.WithTotal)}, + } + if req.Env != nil { + attributes = append(attributes, attribute.KeyValue{Key: "env", Value: attribute.StringValue(*req.Env)}) + } + if req.Source != nil { + attributes = append(attributes, attribute.KeyValue{Key: "source", Value: attribute.StringValue(*req.Source)}) + } + if req.Duration != nil { + attributes = append(attributes, attribute.KeyValue{Key: "duration", Value: attribute.StringValue(req.Duration.String())}) + } + span.SetAttributes(attributes...) + + var duration *time.Duration + if req.Duration != nil { + parsedDuration := req.Duration.AsDuration() + duration = &parsedDuration + } + + request := types.GetTopErrorGroupsRequest{ + Env: req.Env, + Source: req.Source, + Duration: duration, + Limit: req.Limit, + Offset: req.Offset, + WithTotal: req.WithTotal, + } + + groups, total, err := a.service.GetTopErrorGroups(ctx, request) + if err != nil { + return nil, grpcutil.ProcessError(err) + } + + return &errorgroups.GetTopGroupsResponse{ + Total: total, + Groups: topGroupsToProto(groups), + }, nil +} + +func topGroupsToProto(source []types.TopErrorGroup) []*errorgroups.GetTopGroupsResponse_Group { + groups := make([]*errorgroups.GetTopGroupsResponse_Group, 0, len(source)) + + for _, g := range source { + groups = append(groups, &errorgroups.GetTopGroupsResponse_Group{ + Hash: g.Hash, + Message: g.Message, + Source: g.Source, + SeenTotal: g.Count, + }) + } + + return groups +} diff --git a/internal/api/errorgroups/v1/grpc/get_top_groups_test.go b/internal/api/errorgroups/v1/grpc/get_top_groups_test.go new file mode 100644 index 0000000..3105792 --- /dev/null +++ b/internal/api/errorgroups/v1/grpc/get_top_groups_test.go @@ -0,0 +1,140 @@ +package grpc + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/ozontech/seq-ui/internal/app/types" + svc_mock "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups/mock" + errorgroups_v1 "github.com/ozontech/seq-ui/pkg/errorgroups/v1" +) + +func TestGetTopGroups(t *testing.T) { + var ( + env = "test-env" + source = "test-source" + duration = 2 * time.Minute + someErr = errors.New("some err") + ) + + type mockArgs struct { + req types.GetTopErrorGroupsRequest + + groups []types.TopErrorGroup + total uint64 + err error + } + + tests := []struct { + name string + + req *errorgroups_v1.GetTopGroupsRequest + want *errorgroups_v1.GetTopGroupsResponse + wantErr bool + + mockArgs *mockArgs + }{ + { + name: "ok", + + req: &errorgroups_v1.GetTopGroupsRequest{ + Env: &env, + Source: &source, + Duration: durationpb.New(duration), + Limit: 2, + Offset: 0, + WithTotal: true, + }, + want: &errorgroups_v1.GetTopGroupsResponse{ + Total: 10, + Groups: []*errorgroups_v1.GetTopGroupsResponse_Group{ + { + Hash: 123, + Message: "some error 1", + Source: source, + SeenTotal: 10, + }, + { + Hash: 456, + Message: "some error 2", + Source: source, + SeenTotal: 5, + }, + }, + }, + + mockArgs: &mockArgs{ + req: types.GetTopErrorGroupsRequest{ + Env: &env, + Source: &source, + Duration: &duration, + Limit: 2, + Offset: 0, + WithTotal: true, + }, + + groups: []types.TopErrorGroup{ + { + Hash: 123, + Message: "some error 1", + Source: source, + Count: 10, + }, + { + Hash: 456, + Message: "some error 2", + Source: source, + Count: 5, + }, + }, + total: 10, + }, + }, + { + name: "err_svc", + + req: &errorgroups_v1.GetTopGroupsRequest{}, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetTopErrorGroupsRequest{}, + + err: someErr, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockedSvc := svc_mock.NewMockService(ctrl) + + api := New(mockedSvc) + + if ma := tt.mockArgs; ma != nil { + mockedSvc.EXPECT(). + GetTopErrorGroups(gomock.Any(), ma.req). + Return(ma.groups, ma.total, ma.err). + Times(1) + } + + got, err := api.GetTopGroups(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { + return + } + + require.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/api/errorgroups/v1/http/api.go b/internal/api/errorgroups/v1/http/api.go index 98beb00..b8134d0 100644 --- a/internal/api/errorgroups/v1/http/api.go +++ b/internal/api/errorgroups/v1/http/api.go @@ -10,12 +10,12 @@ import ( ) type API struct { - service *errorgroups.Service + service errorgroups.Service } -func New(service *errorgroups.Service) *API { +func New(svc errorgroups.Service) *API { return &API{ - service: service, + service: svc, } } @@ -23,11 +23,12 @@ func (a *API) Router() chi.Router { mux := chi.NewMux() mux.Post("/groups", a.serveGetGroups) + mux.Post("/top_groups", a.serveGetTopGroups) mux.Post("/hist", a.serveGetHist) mux.Post("/details", a.serveGetDetails) mux.Post("/releases", a.serveGetReleases) mux.Post("/services", a.serveGetServices) - mux.Post("/diff_by_releases", a.serveGetDiffByReleases) + mux.Post("/diff_by_releases", a.serveDiffByReleases) return mux } diff --git a/internal/api/errorgroups/v1/http/diff_by_releases.go b/internal/api/errorgroups/v1/http/diff_by_releases.go index 9373795..84905b1 100644 --- a/internal/api/errorgroups/v1/http/diff_by_releases.go +++ b/internal/api/errorgroups/v1/http/diff_by_releases.go @@ -22,7 +22,7 @@ import ( // @Success 200 {object} diffByReleasesResponse "A successful response" // @Failure default {object} httputil.Error "An unexpected error response" // @Security bearer -func (a *API) serveGetDiffByReleases(w http.ResponseWriter, r *http.Request) { +func (a *API) serveDiffByReleases(w http.ResponseWriter, r *http.Request) { ctx, span := tracing.StartSpan(r.Context(), "errorgroups_v1_diff_by_releases") defer span.End() diff --git a/internal/api/errorgroups/v1/http/diff_by_releases_test.go b/internal/api/errorgroups/v1/http/diff_by_releases_test.go new file mode 100644 index 0000000..b369361 --- /dev/null +++ b/internal/api/errorgroups/v1/http/diff_by_releases_test.go @@ -0,0 +1,169 @@ +package http + +import ( + "errors" + "net/http" + "testing" + "time" + + "go.uber.org/mock/gomock" + + "github.com/ozontech/seq-ui/internal/api/httputil" + "github.com/ozontech/seq-ui/internal/app/types" + svc_mock "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups/mock" +) + +func TestServeDiffByReleases(t *testing.T) { + var ( + service = "test-service" + releases = []string{"release1", "release2"} + env = "test-env" + source = "test-source" + now = time.Now() + oneMinuteAgo = now.Add(-1 * time.Minute) + twoMinutesAgo = now.Add(-2 * time.Minute) + someErr = errors.New("some err") + ) + + type mockArgs struct { + req types.DiffByReleasesRequest + + groups []types.DiffGroup + total uint64 + err error + } + + tests := []struct { + name string + + req diffByReleasesRequest + want diffByReleasesResponse + wantErr bool + + mockArgs *mockArgs + }{ + { + name: "ok", + + req: diffByReleasesRequest{ + Service: service, + Releases: releases, + Env: &env, + Source: &source, + Limit: 2, + Offset: 0, + Order: OrderFrequent, + WithTotal: true, + }, + want: diffByReleasesResponse{ + Total: 10, + Groups: []diffGroup{ + { + Hash: "123", + Message: "some error 1", + Source: source, + FirstSeenAt: twoMinutesAgo, + LastSeenAt: oneMinuteAgo, + ReleaseInfos: map[string]diffReleaseInfo{ + "release1": {SeenTotal: 10}, + "release2": {SeenTotal: 20}, + }, + }, + { + Hash: "456", + Message: "some error 2", + Source: source, + FirstSeenAt: twoMinutesAgo, + LastSeenAt: oneMinuteAgo, + ReleaseInfos: map[string]diffReleaseInfo{ + "release1": {SeenTotal: 40}, + "release2": {SeenTotal: 0}, + }, + }, + }, + }, + + mockArgs: &mockArgs{ + req: types.DiffByReleasesRequest{ + Service: service, + Releases: releases, + Env: &env, + Source: &source, + Limit: 2, + Offset: 0, + Order: types.OrderFrequent, + WithTotal: true, + }, + + groups: []types.DiffGroup{ + { + Hash: 123, + Message: "some error 1", + Source: source, + FirstSeenAt: twoMinutesAgo, + LastSeenAt: oneMinuteAgo, + ReleaseInfos: map[string]types.DiffReleaseInfo{ + "release1": {SeenTotal: 10}, + "release2": {SeenTotal: 20}, + }, + }, + { + Hash: 456, + Message: "some error 2", + Source: source, + FirstSeenAt: twoMinutesAgo, + LastSeenAt: oneMinuteAgo, + ReleaseInfos: map[string]types.DiffReleaseInfo{ + "release1": {SeenTotal: 40}, + "release2": {SeenTotal: 0}, + }, + }, + }, + total: 10, + }, + }, + + { + name: "err_svc", + + req: diffByReleasesRequest{}, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.DiffByReleasesRequest{}, + + err: someErr, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockedSvc := svc_mock.NewMockService(ctrl) + + api := New(mockedSvc) + + if ma := tt.mockArgs; ma != nil { + mockedSvc.EXPECT(). + DiffByReleases(gomock.Any(), ma.req). + Return(ma.groups, ma.total, ma.err). + Times(1) + } + + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[diffByReleasesRequest, diffByReleasesResponse]{ + Method: http.MethodPost, + Target: "/errorgroups/v1/diff_by_releases", + Req: tt.req, + + Handler: api.serveDiffByReleases, + + Want: tt.want, + WantErr: tt.wantErr, + }) + }) + } +} diff --git a/internal/api/errorgroups/v1/http/get_details.go b/internal/api/errorgroups/v1/http/get_details.go index e4d2261..b591f54 100644 --- a/internal/api/errorgroups/v1/http/get_details.go +++ b/internal/api/errorgroups/v1/http/get_details.go @@ -41,12 +41,14 @@ func (a *API) serveGetDetails(w http.ResponseWriter, r *http.Request) { } attributes := []attribute.KeyValue{ - {Key: "service", Value: attribute.StringValue(httpReq.Service)}, {Key: "group_hash", Value: attribute.StringValue(httpReq.GroupHash)}, } if httpReq.Env != nil { attributes = append(attributes, attribute.KeyValue{Key: "env", Value: attribute.StringValue(*httpReq.Env)}) } + if httpReq.Service != nil { + attributes = append(attributes, attribute.KeyValue{Key: "service", Value: attribute.StringValue(*httpReq.Service)}) + } if httpReq.Release != nil { attributes = append(attributes, attribute.KeyValue{Key: "release", Value: attribute.StringValue(*httpReq.Release)}) } @@ -56,10 +58,10 @@ func (a *API) serveGetDetails(w http.ResponseWriter, r *http.Request) { span.SetAttributes(attributes...) request := types.GetErrorGroupDetailsRequest{ - Service: httpReq.Service, GroupHash: *parsedGroupHash, Env: httpReq.Env, Source: httpReq.Source, + Service: httpReq.Service, Release: httpReq.Release, } details, err := a.service.GetDetails(ctx, request) @@ -77,15 +79,13 @@ func (a *API) serveGetDetails(w http.ResponseWriter, r *http.Request) { Source: details.Source, LogTags: details.LogTags, Distributions: newDistributions(details.Distributions), - - Envs: newEnvs(details.Distributions.ByEnv), // deprecated }) } type getDetailsRequest struct { - Service string `json:"service"` GroupHash string `json:"group_hash" format:"uint64"` Env *string `json:"env,omitempty"` + Service *string `json:"service,omitempty"` Release *string `json:"release,omitempty"` Source *string `json:"source,omitempty"` } // @name errorgroups.v1.GetDetailsRequest @@ -99,8 +99,6 @@ type getDetailsResponse struct { Source string `json:"source"` LogTags map[string]string `json:"log_tags,omitempty"` Distributions distributions `json:"distributions"` - - Envs []env `json:"envs"` // Deprecated. Use `distributions.by_envs` instead } // @name errorgroups.v1.GetDetailsResponse type distribution struct { @@ -110,46 +108,32 @@ type distribution struct { type distributions struct { ByEnv []distribution `json:"by_env"` + BySource []distribution `json:"by_source"` + ByService []distribution `json:"by_service"` ByRelease []distribution `json:"by_release"` } // @name errorgroups.v1.Distributions func newDistributions(source types.ErrorGroupDistributions) distributions { - newDistr := func(d types.ErrorGroupDistribution) distribution { - return distribution{ - Value: d.Value, - Percent: d.Percent, + newDistr := func(ds []types.ErrorGroupDistribution) []distribution { + if len(ds) == 0 { + return nil } - } - ds := distributions{ - ByEnv: make([]distribution, 0, len(source.ByEnv)), - ByRelease: make([]distribution, 0, len(source.ByRelease)), - } + res := make([]distribution, 0, len(ds)) + for _, d := range ds { + res = append(res, distribution{ + Value: d.Value, + Percent: d.Percent, + }) + } - for _, d := range source.ByEnv { - ds.ByEnv = append(ds.ByEnv, newDistr(d)) + return res } - for _, d := range source.ByRelease { - ds.ByRelease = append(ds.ByRelease, newDistr(d)) - } - - return ds -} -type env struct { - Env string `json:"env"` - Percent uint64 `json:"percent"` -} // @name errorgroups.v1.Env - -func newEnvs(source []types.ErrorGroupDistribution) []env { - envs := make([]env, 0, len(source)) - - for _, e := range source { - envs = append(envs, env{ - Env: e.Value, - Percent: e.Percent, - }) + return distributions{ + ByEnv: newDistr(source.ByEnv), + BySource: newDistr(source.BySource), + ByService: newDistr(source.ByService), + ByRelease: newDistr(source.ByRelease), } - - return envs } diff --git a/internal/api/errorgroups/v1/http/get_details_test.go b/internal/api/errorgroups/v1/http/get_details_test.go index 24a6567..df78a97 100644 --- a/internal/api/errorgroups/v1/http/get_details_test.go +++ b/internal/api/errorgroups/v1/http/get_details_test.go @@ -1,129 +1,147 @@ package http import ( + "errors" "fmt" "net/http" - "net/http/httptest" - "strconv" - "strings" "testing" "time" "go.uber.org/mock/gomock" "github.com/ozontech/seq-ui/internal/api/httputil" - "github.com/ozontech/seq-ui/internal/app/config" "github.com/ozontech/seq-ui/internal/app/types" - repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository_ch/mock" - "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups" + svc_mock "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups/mock" ) func TestServeGetDetails(t *testing.T) { var ( - service = "service1" - groupHash uint64 = 123 - env = "prod" - release = "test_release" - now = time.Now() - oneMinuteAgo = now.Add(-1 * time.Minute) - twoMinutesAgo = now.Add(-2 * time.Minute) + service = "test-service" + groupHash = uint64(123) + msg = "some error" + env = "test-env" + source = "test-source" + release = "test-release" + now = time.Now() + oneMinuteAgo = now.Add(-1 * time.Minute) + twoMinutesAgo = now.Add(-2 * time.Minute) + someErr = errors.New("some err") ) type mockArgs struct { - req types.GetErrorGroupDetailsRequest - detailsResp types.ErrorGroupDetails - countsResp *types.ErrorGroupCounts - err error + req types.GetErrorGroupDetailsRequest + + details types.ErrorGroupDetails + err error } tests := []struct { name string - reqBody string - wantRespBody string - wantStatus int + req getDetailsRequest + want getDetailsResponse + wantErr bool mockArgs *mockArgs }{ { - name: "success_all_fields", - reqBody: fmt.Sprintf( - `{"service":"%s","group_hash":"%d","env":"%s","release":"%s"}`, - service, groupHash, env, release, - ), - wantRespBody: fmt.Sprintf( - `{"group_hash":"123","message":"some error","seen_total":10,"first_seen_at":"%s","last_seen_at":"%s","source":"","distributions":{"by_env":[{"value":"prod","percent":100}],"by_release":[{"value":"test_release","percent":100}]},"envs":[{"env":"prod","percent":100}]}`, - twoMinutesAgo.Format(time.RFC3339Nano), oneMinuteAgo.Format(time.RFC3339Nano), - ), - wantStatus: http.StatusOK, + name: "ok", + + req: getDetailsRequest{ + GroupHash: fmt.Sprintf("%d", groupHash), + Env: &env, + Source: &source, + Service: &service, + Release: &release, + }, + want: getDetailsResponse{ + GroupHash: fmt.Sprintf("%d", groupHash), + Message: msg, + Source: source, + SeenTotal: 10, + FirstSeenAt: twoMinutesAgo, + LastSeenAt: oneMinuteAgo, + LogTags: map[string]string{ + "tag1": "val1", + "tag2": "val2", + }, + Distributions: distributions{ + ByEnv: []distribution{ + {Value: "env1", Percent: 70}, + {Value: "env2", Percent: 30}, + }, + BySource: []distribution{ + {Value: "source1", Percent: 50}, + {Value: "source2", Percent: 50}, + }, + ByService: []distribution{ + {Value: "service1", Percent: 100}, + {Value: "service2", Percent: 0}, + }, + ByRelease: []distribution{ + {Value: "release1", Percent: 60}, + {Value: "release2", Percent: 40}, + }, + }, + }, + mockArgs: &mockArgs{ req: types.GetErrorGroupDetailsRequest{ - Service: service, GroupHash: groupHash, + Service: &service, Env: &env, + Source: &source, Release: &release, }, - detailsResp: types.ErrorGroupDetails{ - GroupHash: 123, - Message: "some error", + + details: types.ErrorGroupDetails{ + Hash: groupHash, + Message: msg, SeenTotal: 10, FirstSeenAt: twoMinutesAgo, LastSeenAt: oneMinuteAgo, + Source: source, + LogTags: map[string]string{ + "tag1": "val1", + "tag2": "val2", + }, + Distributions: types.ErrorGroupDistributions{ + ByEnv: []types.ErrorGroupDistribution{ + {Value: "env1", Percent: 70}, + {Value: "env2", Percent: 30}, + }, + BySource: []types.ErrorGroupDistribution{ + {Value: "source1", Percent: 50}, + {Value: "source2", Percent: 50}, + }, + ByService: []types.ErrorGroupDistribution{ + {Value: "service1", Percent: 100}, + {Value: "service2", Percent: 0}, + }, + ByRelease: []types.ErrorGroupDistribution{ + {Value: "release1", Percent: 60}, + {Value: "release2", Percent: 40}, + }, + }, }, }, }, { - name: "success_only_required_fields", - reqBody: fmt.Sprintf( - `{"service":"%s","group_hash":"%s"}`, - service, strconv.FormatUint(groupHash, 10), - ), - wantRespBody: fmt.Sprintf( - `{"group_hash":"123","message":"some error","seen_total":10,"first_seen_at":"%s","last_seen_at":"%s","source":"","distributions":{"by_env":[{"value":"env1","percent":70},{"value":"env2","percent":30}],"by_release":[{"value":"release2","percent":60},{"value":"release1","percent":40}]},"envs":[{"env":"env1","percent":70},{"env":"env2","percent":30}]}`, - twoMinutesAgo.Format(time.RFC3339Nano), oneMinuteAgo.Format(time.RFC3339Nano), - ), - wantStatus: http.StatusOK, + name: "err_svc", + + req: getDetailsRequest{ + GroupHash: fmt.Sprintf("%d", groupHash), + }, + wantErr: true, + mockArgs: &mockArgs{ req: types.GetErrorGroupDetailsRequest{ - Service: service, GroupHash: groupHash, }, - detailsResp: types.ErrorGroupDetails{ - GroupHash: 123, - Message: "some error", - SeenTotal: 10, - FirstSeenAt: twoMinutesAgo, - LastSeenAt: oneMinuteAgo, - }, - countsResp: &types.ErrorGroupCounts{ - ByEnv: types.ErrorGroupCount{ - "env1": 7, - "env2": 3, - }, - ByRelease: types.ErrorGroupCount{ - "release1": 4, - "release2": 6, - }, - }, + + err: someErr, }, }, - { - name: "err_no_service_field", - reqBody: `{"group_hash":"123"}`, - wantRespBody: `{"message":"invalid request field: 'service' must not be empty"}`, - wantStatus: http.StatusBadRequest, - }, - { - name: "err_no_group_hash_field", - reqBody: `{"service":"service1"}`, - wantRespBody: `{"message":"failed to parse group_hash: strconv.ParseUint: parsing \"\": invalid syntax"}`, - wantStatus: http.StatusBadRequest, - }, - { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, - }, } for _, tt := range tests { @@ -132,25 +150,26 @@ func TestServeGetDetails(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - mockedRepo := repo_mock.NewMockRepository(ctrl) - api := New(errorgroups.New(mockedRepo, config.LogTagsMapping{})) - - if tt.mockArgs != nil { - mockedRepo.EXPECT().GetErrorDetails(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.detailsResp, tt.mockArgs.err).Times(1) - if tt.mockArgs.countsResp != nil { - mockedRepo.EXPECT().GetErrorCounts(gomock.Any(), tt.mockArgs.req). - Return(*tt.mockArgs.countsResp, tt.mockArgs.err).Times(1) - } + mockedSvc := svc_mock.NewMockService(ctrl) + + api := New(mockedSvc) + + if ma := tt.mockArgs; ma != nil { + mockedSvc.EXPECT(). + GetDetails(gomock.Any(), ma.req). + Return(ma.details, ma.err). + Times(1) } - req := httptest.NewRequest(http.MethodPost, "/errorgroups/v1/details", strings.NewReader(tt.reqBody)) + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[getDetailsRequest, getDetailsResponse]{ + Method: http.MethodPost, + Target: "/errorgroups/v1/details", + Req: tt.req, + + Handler: api.serveGetDetails, - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetDetails, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + Want: tt.want, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/errorgroups/v1/http/get_groups.go b/internal/api/errorgroups/v1/http/get_groups.go index fb2c59d..3bfc2dc 100644 --- a/internal/api/errorgroups/v1/http/get_groups.go +++ b/internal/api/errorgroups/v1/http/get_groups.go @@ -160,7 +160,7 @@ func newGroups(source []types.ErrorGroup) []group { groups = append(groups, group{ Hash: strconv.FormatUint(g.Hash, 10), Message: g.Message, - SeenTotal: g.SeenTotal, + SeenTotal: g.Count, FirstSeenAt: g.FirstSeenAt, LastSeenAt: g.LastSeenAt, Source: g.Source, diff --git a/internal/api/errorgroups/v1/http/get_groups_test.go b/internal/api/errorgroups/v1/http/get_groups_test.go index bc26e46..5a51807 100644 --- a/internal/api/errorgroups/v1/http/get_groups_test.go +++ b/internal/api/errorgroups/v1/http/get_groups_test.go @@ -1,135 +1,219 @@ package http import ( - "fmt" + "errors" "net/http" - "net/http/httptest" - "strings" "testing" "time" "go.uber.org/mock/gomock" "github.com/ozontech/seq-ui/internal/api/httputil" - "github.com/ozontech/seq-ui/internal/app/config" "github.com/ozontech/seq-ui/internal/app/types" - repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository_ch/mock" - "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups" + svc_mock "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups/mock" ) func TestServeGetGroups(t *testing.T) { var ( - service = "service1" - env = "prod" - release = "v1" + service = "test-service" + env = "test-env" + release = "test-release" + source = "test-source" + durationStr = "2m" duration = 2 * time.Minute now = time.Now() oneMinuteAgo = now.Add(-1 * time.Minute) twoMinutesAgo = now.Add(-2 * time.Minute) + someErr = errors.New("some err") ) type mockArgs struct { - req types.GetErrorGroupsRequest - groupsResp []types.ErrorGroup - countsResp uint64 - err error + req types.GetErrorGroupsRequest + + groups []types.ErrorGroup + total uint64 + err error } tests := []struct { name string - reqBody string - wantRespBody string - wantStatus int + req getGroupsRequest + want getGroupsResponse + wantErr bool mockArgs *mockArgs }{ { - name: "success_all_fields", - reqBody: fmt.Sprintf( - `{"service":"%s","env":"%s","release":"%s","duration":"%s","limit":10,"offset":0,"order":"%s","with_total":true}`, - service, env, release, duration, OrderOldest, - ), - wantRespBody: fmt.Sprintf( - `{"total":2,"groups":[{"hash":"123","message":"some error 1","seen_total":10,"first_seen_at":"%s","last_seen_at":"%s","source":""},{"hash":"456","message":"some error 2","seen_total":5,"first_seen_at":"%s","last_seen_at":"%s","source":""}]}`, - twoMinutesAgo.Format(time.RFC3339Nano), oneMinuteAgo.Format(time.RFC3339Nano), - twoMinutesAgo.Format(time.RFC3339Nano), oneMinuteAgo.Format(time.RFC3339Nano), - ), - wantStatus: http.StatusOK, + name: "ok", + + req: getGroupsRequest{ + Service: service, + Env: &env, + Source: &source, + Release: &release, + Duration: &durationStr, + Limit: 2, + Offset: 0, + Order: OrderFrequent, + WithTotal: true, + }, + want: getGroupsResponse{ + Total: 10, + Groups: []group{ + { + Hash: "123", + Message: "some error 1", + Source: source, + SeenTotal: 5, + FirstSeenAt: twoMinutesAgo, + LastSeenAt: oneMinuteAgo, + }, + { + Hash: "456", + Message: "some error 2", + Source: source, + SeenTotal: 10, + FirstSeenAt: twoMinutesAgo, + LastSeenAt: oneMinuteAgo, + }, + }, + }, + mockArgs: &mockArgs{ req: types.GetErrorGroupsRequest{ Service: service, Env: &env, + Source: &source, Release: &release, Duration: &duration, - Limit: 10, + Limit: 2, Offset: 0, - Order: types.OrderOldest, + Order: types.OrderFrequent, WithTotal: true, }, - groupsResp: []types.ErrorGroup{ + + groups: []types.ErrorGroup{ { Hash: 123, Message: "some error 1", - SeenTotal: 10, + Source: source, + Count: 5, FirstSeenAt: twoMinutesAgo, LastSeenAt: oneMinuteAgo, }, { Hash: 456, Message: "some error 2", - SeenTotal: 5, + Source: source, + Count: 10, FirstSeenAt: twoMinutesAgo, LastSeenAt: oneMinuteAgo, }, }, - countsResp: 2, + total: 10, }, }, { - name: "success_only_required_fields", - reqBody: fmt.Sprintf(`{"service":"%s"}`, service), - wantRespBody: fmt.Sprintf( - `{"total":0,"groups":[{"hash":"123","message":"some error 1","seen_total":10,"first_seen_at":"%s","last_seen_at":"%s","source":""},{"hash":"456","message":"some error 2","seen_total":5,"first_seen_at":"%s","last_seen_at":"%s","source":""}]}`, - twoMinutesAgo.Format(time.RFC3339Nano), oneMinuteAgo.Format(time.RFC3339Nano), - twoMinutesAgo.Format(time.RFC3339Nano), oneMinuteAgo.Format(time.RFC3339Nano), - ), - wantStatus: http.StatusOK, + name: "ok_new", + + req: getGroupsRequest{ + Service: service, + Env: &env, + Source: &source, + Release: &release, + Duration: &durationStr, + Limit: 2, + Offset: 0, + Order: OrderFrequent, + WithTotal: true, + Filter: &groupsFilter{ + IsNew: true, + }, + }, + want: getGroupsResponse{ + Total: 10, + Groups: []group{ + { + Hash: "123", + Message: "some error 1", + Source: source, + SeenTotal: 5, + FirstSeenAt: twoMinutesAgo, + LastSeenAt: oneMinuteAgo, + }, + { + Hash: "456", + Message: "some error 2", + Source: source, + SeenTotal: 10, + FirstSeenAt: twoMinutesAgo, + LastSeenAt: oneMinuteAgo, + }, + }, + }, + mockArgs: &mockArgs{ req: types.GetErrorGroupsRequest{ - Service: service, - Limit: 25, - Offset: 0, - Order: types.OrderFrequent, + Service: service, + Env: &env, + Source: &source, + Release: &release, + Duration: &duration, + Limit: 2, + Offset: 0, + Order: types.OrderFrequent, + WithTotal: true, }, - groupsResp: []types.ErrorGroup{ + + groups: []types.ErrorGroup{ { Hash: 123, Message: "some error 1", - SeenTotal: 10, + Source: source, + Count: 5, FirstSeenAt: twoMinutesAgo, LastSeenAt: oneMinuteAgo, }, { Hash: 456, Message: "some error 2", - SeenTotal: 5, + Source: source, + Count: 10, FirstSeenAt: twoMinutesAgo, LastSeenAt: oneMinuteAgo, }, }, + total: 10, }, }, { - name: "err_no_service_field", - reqBody: `{"group_hash":"123"}`, - wantRespBody: `{"message":"invalid request field: 'service' must not be empty"}`, - wantStatus: http.StatusBadRequest, + name: "err_svc", + + req: getGroupsRequest{}, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupsRequest{}, + + err: someErr, + }, }, { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, + name: "err_svc_new", + + req: getGroupsRequest{ + Filter: &groupsFilter{ + IsNew: true, + }, + }, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupsRequest{}, + + err: someErr, + }, }, } @@ -139,25 +223,33 @@ func TestServeGetGroups(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - mockedRepo := repo_mock.NewMockRepository(ctrl) - api := New(errorgroups.New(mockedRepo, config.LogTagsMapping{})) - - if tt.mockArgs != nil { - mockedRepo.EXPECT().GetErrorGroups(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.groupsResp, tt.mockArgs.err).Times(1) - if tt.mockArgs.req.WithTotal { - mockedRepo.EXPECT().GetErrorGroupsCount(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.countsResp, tt.mockArgs.err).Times(1) + mockedSvc := svc_mock.NewMockService(ctrl) + + api := New(mockedSvc) + + if ma := tt.mockArgs; ma != nil { + if tt.req.Filter != nil && tt.req.Filter.IsNew { + mockedSvc.EXPECT(). + GetNewErrorGroups(gomock.Any(), ma.req). + Return(ma.groups, ma.total, ma.err). + Times(1) + } else { + mockedSvc.EXPECT(). + GetErrorGroups(gomock.Any(), ma.req). + Return(ma.groups, ma.total, ma.err). + Times(1) } } - req := httptest.NewRequest(http.MethodPost, "/errorgroups/v1/groups", strings.NewReader(tt.reqBody)) + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[getGroupsRequest, getGroupsResponse]{ + Method: http.MethodPost, + Target: "/errorgroups/v1/groups", + Req: tt.req, + + Handler: api.serveGetGroups, - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetGroups, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + Want: tt.want, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/errorgroups/v1/http/get_hist.go b/internal/api/errorgroups/v1/http/get_hist.go index aaca1b4..6236b70 100644 --- a/internal/api/errorgroups/v1/http/get_hist.go +++ b/internal/api/errorgroups/v1/http/get_hist.go @@ -46,13 +46,12 @@ func (a *API) serveGetHist(w http.ResponseWriter, r *http.Request) { return } - attributes := []attribute.KeyValue{ - {Key: "service", Value: attribute.StringValue(httpReq.Service)}, - } + attributes := []attribute.KeyValue{} if httpReq.GroupHash != nil { - attributes = append(attributes, attribute.KeyValue{ - Key: "group_hash", Value: attribute.StringValue(*httpReq.GroupHash), - }) + attributes = append(attributes, attribute.KeyValue{Key: "group_hash", Value: attribute.StringValue(*httpReq.GroupHash)}) + } + if httpReq.Service != nil { + attributes = append(attributes, attribute.KeyValue{Key: "service", Value: attribute.StringValue(*httpReq.Service)}) } if httpReq.Env != nil { attributes = append(attributes, attribute.KeyValue{Key: "env", Value: attribute.StringValue(*httpReq.Env)}) @@ -88,7 +87,7 @@ func (a *API) serveGetHist(w http.ResponseWriter, r *http.Request) { } type getHistRequest struct { - Service string `json:"service"` + Service *string `json:"service"` GroupHash *string `json:"group_hash,omitempty" format:"uint64"` Env *string `json:"env,omitempty"` Source *string `json:"source,omitempty"` diff --git a/internal/api/errorgroups/v1/http/get_hist_test.go b/internal/api/errorgroups/v1/http/get_hist_test.go index 2106bdf..6005643 100644 --- a/internal/api/errorgroups/v1/http/get_hist_test.go +++ b/internal/api/errorgroups/v1/http/get_hist_test.go @@ -1,104 +1,102 @@ package http import ( - "fmt" + "errors" "net/http" - "net/http/httptest" - "strconv" - "strings" "testing" "time" "go.uber.org/mock/gomock" "github.com/ozontech/seq-ui/internal/api/httputil" - "github.com/ozontech/seq-ui/internal/app/config" "github.com/ozontech/seq-ui/internal/app/types" - repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository_ch/mock" - "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups" + svc_mock "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups/mock" ) func TestServeGetHist(t *testing.T) { var ( - service = "service1" - groupHash uint64 = 123 - env = "prod" - release = "v1" - twoMinutes = 2 * time.Minute - now = time.Now() - oneMinuteAgo = now.Add(-1 * time.Minute) - twoMinutesAgo = now.Add(-2 * time.Minute) + groupHashStr = "123" + groupHash = uint64(123) + service = "test-service" + env = "test-env" + source = "test-source" + release = "test-release" + durationStr = "2m" + duration = 2 * time.Minute + now = time.Now() + oneMinuteAgo = now.Add(-1 * time.Minute) + twoMinutesAgo = now.Add(-2 * time.Minute) + someErr = errors.New("some err") ) type mockArgs struct { - req types.GetErrorHistRequest - resp []types.ErrorHistBucket - err error + req types.GetErrorHistRequest + + buckets []types.ErrorHistBucket + err error } tests := []struct { name string - reqBody string - wantRespBody string - wantStatus int + req getHistRequest + want getHistResponse + wantErr bool mockArgs *mockArgs }{ { - name: "success_all_fields", - reqBody: fmt.Sprintf( - `{"service":"%s","group_hash":"%s","env":"%s","release":"%s","duration":"%s"}`, - service, strconv.FormatUint(groupHash, 10), env, release, twoMinutes, - ), - wantRespBody: fmt.Sprintf( - `{"buckets":[{"time":"%s","count":10},{"time":"%s","count":20}]}`, - oneMinuteAgo.Format(time.RFC3339Nano), twoMinutesAgo.Format(time.RFC3339Nano), - ), - wantStatus: http.StatusOK, + name: "ok", + + req: getHistRequest{ + GroupHash: &groupHashStr, + Service: &service, + Env: &env, + Source: &source, + Release: &release, + Duration: &durationStr, + }, + want: getHistResponse{ + Buckets: []bucket{ + { + Time: twoMinutesAgo, + Count: 100, + }, + { + Time: oneMinuteAgo, + Count: 200, + }, + }, + }, + mockArgs: &mockArgs{ req: types.GetErrorHistRequest{ - Service: service, GroupHash: &groupHash, + Service: &service, Env: &env, + Source: &source, Release: &release, - Duration: &twoMinutes, + Duration: &duration, }, - resp: []types.ErrorHistBucket{ - {Time: oneMinuteAgo, Count: 10}, - {Time: twoMinutesAgo, Count: 20}, + + buckets: []types.ErrorHistBucket{ + {Time: twoMinutesAgo, Count: 100}, + {Time: oneMinuteAgo, Count: 200}, }, }, }, { - name: "success_only_service_field", - reqBody: fmt.Sprintf(`{"service":"%s"}`, service), - wantRespBody: fmt.Sprintf( - `{"buckets":[{"time":"%s","count":10},{"time":"%s","count":20}]}`, - oneMinuteAgo.Format(time.RFC3339Nano), twoMinutesAgo.Format(time.RFC3339Nano), - ), - wantStatus: http.StatusOK, + name: "err_svc", + + req: getHistRequest{}, + wantErr: true, + mockArgs: &mockArgs{ - req: types.GetErrorHistRequest{ - Service: service, - }, - resp: []types.ErrorHistBucket{ - {Time: oneMinuteAgo, Count: 10}, - {Time: twoMinutesAgo, Count: 20}, - }, + req: types.GetErrorHistRequest{}, + + err: someErr, }, }, - { - name: "err_no_service_field", - reqBody: `{"group_hash":"123"}`, - wantRespBody: `{"message":"invalid request field: 'service' must not be empty"}`, - wantStatus: http.StatusBadRequest, - }, - { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, - }, } for _, tt := range tests { @@ -107,21 +105,26 @@ func TestServeGetHist(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - mockedRepo := repo_mock.NewMockRepository(ctrl) - api := New(errorgroups.New(mockedRepo, config.LogTagsMapping{})) + mockedSvc := svc_mock.NewMockService(ctrl) - if tt.mockArgs != nil { - mockedRepo.EXPECT().GetErrorHist(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) + api := New(mockedSvc) + + if ma := tt.mockArgs; ma != nil { + mockedSvc.EXPECT(). + GetHist(gomock.Any(), ma.req). + Return(ma.buckets, ma.err). + Times(1) } - req := httptest.NewRequest(http.MethodPost, "/errorgroups/v1/hist", strings.NewReader(tt.reqBody)) + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[getHistRequest, getHistResponse]{ + Method: http.MethodPost, + Target: "/errorgroups/v1/hist", + Req: tt.req, + + Handler: api.serveGetHist, - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetHist, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + Want: tt.want, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/errorgroups/v1/http/get_releases.go b/internal/api/errorgroups/v1/http/get_releases.go index bdbf170..2a49974 100644 --- a/internal/api/errorgroups/v1/http/get_releases.go +++ b/internal/api/errorgroups/v1/http/get_releases.go @@ -41,7 +41,7 @@ func (a *API) serveGetReleases(w http.ResponseWriter, r *http.Request) { } span.SetAttributes(attributes...) - req := types.GetErrorGroupReleasesRequest{ + req := types.GetReleasesRequest{ Service: httpReq.Service, Env: httpReq.Env, } diff --git a/internal/api/errorgroups/v1/http/get_releases_test.go b/internal/api/errorgroups/v1/http/get_releases_test.go index 3100312..7811b5f 100644 --- a/internal/api/errorgroups/v1/http/get_releases_test.go +++ b/internal/api/errorgroups/v1/http/get_releases_test.go @@ -1,89 +1,72 @@ package http import ( + "errors" "net/http" - "net/http/httptest" - "strings" "testing" "go.uber.org/mock/gomock" "github.com/ozontech/seq-ui/internal/api/httputil" - "github.com/ozontech/seq-ui/internal/app/config" "github.com/ozontech/seq-ui/internal/app/types" - repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository_ch/mock" - "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups" + svc_mock "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups/mock" ) func TestServeGetReleases(t *testing.T) { var ( - service = "service1" - env = "prod" + service = "test-service" + env = "test-env" + someErr = errors.New("some err") ) type mockArgs struct { - req types.GetErrorGroupReleasesRequest - resp []string - err error + req types.GetReleasesRequest + + releases []string + err error } tests := []struct { name string - reqBody string - wantRespBody string - wantStatus int + req getReleasesRequest + want getReleasesResponse + wantErr bool mockArgs *mockArgs }{ { - name: "success_all_fields", - reqBody: `{"service":"service1","env":"prod"}`, - wantRespBody: `{"releases":["v1","v2"]}`, - wantStatus: http.StatusOK, - mockArgs: &mockArgs{ - req: types.GetErrorGroupReleasesRequest{ - Service: service, - Env: &env, - }, - resp: []string{"v1", "v2"}, + name: "ok", + + req: getReleasesRequest{ + Service: service, + Env: &env, }, - }, - { - name: "success_no_env_field", - reqBody: `{"service":"service1","group_hash":"123"}`, - wantRespBody: `{"releases":["v1","v2"]}`, - wantStatus: http.StatusOK, + want: getReleasesResponse{ + Releases: []string{"release1", "release2"}, + }, + mockArgs: &mockArgs{ - req: types.GetErrorGroupReleasesRequest{ + req: types.GetReleasesRequest{ Service: service, + Env: &env, }, - resp: []string{"v1", "v2"}, + + releases: []string{"release1", "release2"}, }, }, { - name: "success_only_service_field", - reqBody: `{"service":"service1"}`, - wantRespBody: `{"releases":["v1","v2"]}`, - wantStatus: http.StatusOK, + name: "err_svc", + + req: getReleasesRequest{}, + wantErr: true, + mockArgs: &mockArgs{ - req: types.GetErrorGroupReleasesRequest{ - Service: service, - }, - resp: []string{"v1", "v2"}, + req: types.GetReleasesRequest{}, + + err: someErr, }, }, - { - name: "err_no_service_field", - reqBody: `{"group_hash":"123"}`, - wantRespBody: `{"message":"invalid request field: 'service' must not be empty"}`, - wantStatus: http.StatusBadRequest, - }, - { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, - }, } for _, tt := range tests { @@ -92,21 +75,26 @@ func TestServeGetReleases(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - mockedRepo := repo_mock.NewMockRepository(ctrl) - api := New(errorgroups.New(mockedRepo, config.LogTagsMapping{})) + mockedSvc := svc_mock.NewMockService(ctrl) - if tt.mockArgs != nil { - mockedRepo.EXPECT().GetErrorReleases(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) + api := New(mockedSvc) + + if ma := tt.mockArgs; ma != nil { + mockedSvc.EXPECT(). + GetReleases(gomock.Any(), ma.req). + Return(ma.releases, ma.err). + Times(1) } - req := httptest.NewRequest(http.MethodPost, "/errorgroups/v1/releases", strings.NewReader(tt.reqBody)) + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[getReleasesRequest, getReleasesResponse]{ + Method: http.MethodPost, + Target: "/errorgroups/v1/releases", + Req: tt.req, + + Handler: api.serveGetReleases, - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetReleases, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + Want: tt.want, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/errorgroups/v1/http/get_services_test.go b/internal/api/errorgroups/v1/http/get_services_test.go index c60c22a..7ae79c2 100644 --- a/internal/api/errorgroups/v1/http/get_services_test.go +++ b/internal/api/errorgroups/v1/http/get_services_test.go @@ -1,93 +1,75 @@ package http import ( + "errors" "net/http" - "net/http/httptest" - "strings" "testing" "go.uber.org/mock/gomock" "github.com/ozontech/seq-ui/internal/api/httputil" - "github.com/ozontech/seq-ui/internal/app/config" "github.com/ozontech/seq-ui/internal/app/types" - repo_mock "github.com/ozontech/seq-ui/internal/pkg/repository_ch/mock" - "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups" + svc_mock "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups/mock" ) func TestServeGetServices(t *testing.T) { var ( - query = "itc" - env = "prod" + env = "test-env" + someErr = errors.New("some err") ) type mockArgs struct { - req types.GetServicesRequest - resp []string - err error + req types.GetServicesRequest + + services []string + err error } tests := []struct { name string - reqBody string - wantRespBody string - wantStatus int + req getServicesRequest + want getServicesResponse + wantErr bool mockArgs *mockArgs }{ { - name: "success_all_fields", - reqBody: `{"query":"itc","env":"prod"}`, - wantRespBody: `{"services":["service1","service2"]}`, - wantStatus: http.StatusOK, - mockArgs: &mockArgs{ - req: types.GetServicesRequest{ - Query: query, - Env: &env, - }, - resp: []string{"service1", "service2"}, + name: "ok", + + req: getServicesRequest{ + Query: "test", + Env: &env, + Limit: 10, + Offset: 20, }, - }, - { - name: "success_no_env_field", - reqBody: `{"query":"itc"}`, - wantRespBody: `{"services":["service1","service2"]}`, - wantStatus: http.StatusOK, - mockArgs: &mockArgs{ - req: types.GetServicesRequest{ - Query: query, - }, - resp: []string{"service1", "service2"}, + want: getServicesResponse{ + Services: []string{"service1", "service2"}, }, - }, - { - name: "success_no_query_field", - reqBody: `{"query":"","env":"prod"}`, - wantRespBody: `{"services":["service1","service2"]}`, - wantStatus: http.StatusOK, + mockArgs: &mockArgs{ req: types.GetServicesRequest{ - Env: &env, + Query: "test", + Env: &env, + Limit: 10, + Offset: 20, }, - resp: []string{"service1", "service2"}, + + services: []string{"service1", "service2"}, }, }, { - name: "success_no_query_no_env", - reqBody: `{"query":""}`, - wantRespBody: `{"services":["service1","service2"]}`, - wantStatus: http.StatusOK, + name: "err_svc", + + req: getServicesRequest{}, + wantErr: true, + mockArgs: &mockArgs{ - req: types.GetServicesRequest{}, - resp: []string{"service1", "service2"}, + req: types.GetServicesRequest{}, + + err: someErr, }, }, - { - name: "err_invalid_request", - reqBody: "invalid-request", - wantStatus: http.StatusBadRequest, - }, } for _, tt := range tests { @@ -96,21 +78,26 @@ func TestServeGetServices(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - mockedRepo := repo_mock.NewMockRepository(ctrl) - api := New(errorgroups.New(mockedRepo, config.LogTagsMapping{})) + mockedSvc := svc_mock.NewMockService(ctrl) - if tt.mockArgs != nil { - mockedRepo.EXPECT().GetServices(gomock.Any(), tt.mockArgs.req). - Return(tt.mockArgs.resp, tt.mockArgs.err).Times(1) + api := New(mockedSvc) + + if ma := tt.mockArgs; ma != nil { + mockedSvc.EXPECT(). + GetServices(gomock.Any(), ma.req). + Return(ma.services, ma.err). + Times(1) } - req := httptest.NewRequest(http.MethodPost, "/errorgroups/v1/services", strings.NewReader(tt.reqBody)) + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[getServicesRequest, getServicesResponse]{ + Method: http.MethodPost, + Target: "/errorgroups/v1/services", + Req: tt.req, + + Handler: api.serveGetServices, - httputil.DoTestHTTP(t, httputil.TestDataHTTP{ - Req: req, - Handler: api.serveGetServices, - WantRespBody: tt.wantRespBody, - WantStatus: tt.wantStatus, + Want: tt.want, + WantErr: tt.wantErr, }) }) } diff --git a/internal/api/errorgroups/v1/http/get_top_groups.go b/internal/api/errorgroups/v1/http/get_top_groups.go new file mode 100644 index 0000000..60c7611 --- /dev/null +++ b/internal/api/errorgroups/v1/http/get_top_groups.go @@ -0,0 +1,115 @@ +package http + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + "go.opentelemetry.io/otel/attribute" + + "github.com/ozontech/seq-ui/internal/api/httputil" + "github.com/ozontech/seq-ui/internal/app/types" + "github.com/ozontech/seq-ui/tracing" +) + +// serveGetTopGroups go doc. +// +// @Router /errorgroups/v1/top_groups [post] +// @ID errorgroups_v1_get_top_groups +// @Tags errorgroups_v1 +// @Param body body getTopGroupsRequest true "Request body" +// @Success 200 {object} getTopGroupsResponse "A successful response" +// @Failure default {object} httputil.Error "An unexpected error response" +// @Security bearer +func (a *API) serveGetTopGroups(w http.ResponseWriter, r *http.Request) { + ctx, span := tracing.StartSpan(r.Context(), "errorgroups_v1_get_top_groups") + defer span.End() + + wr := httputil.NewWriter(w) + + var httpReq getTopGroupsRequest + if err := json.NewDecoder(r.Body).Decode(&httpReq); err != nil { + wr.Error(fmt.Errorf("failed to parse request: %w", err), http.StatusBadRequest) + return + } + + parsedDuration, err := parseDuration(httpReq.Duration) + if err != nil { + wr.Error(fmt.Errorf("failed to parse duration: %w", err), http.StatusBadRequest) + return + } + + attributes := []attribute.KeyValue{ + {Key: "limit", Value: attribute.IntValue(int(httpReq.Limit))}, + {Key: "offset", Value: attribute.IntValue(int(httpReq.Offset))}, + {Key: "with_total", Value: attribute.BoolValue(httpReq.WithTotal)}, + } + if httpReq.Env != nil { + attributes = append(attributes, attribute.KeyValue{Key: "env", Value: attribute.StringValue(*httpReq.Env)}) + } + if httpReq.Source != nil { + attributes = append(attributes, attribute.KeyValue{Key: "source", Value: attribute.StringValue(*httpReq.Source)}) + } + if httpReq.Duration != nil { + attributes = append(attributes, attribute.KeyValue{Key: "duration", Value: attribute.StringValue(*httpReq.Duration)}) + } + span.SetAttributes(attributes...) + + req := types.GetTopErrorGroupsRequest{ + Env: httpReq.Env, + Source: httpReq.Source, + Duration: parsedDuration, + Limit: httpReq.Limit, + Offset: httpReq.Offset, + WithTotal: httpReq.WithTotal, + } + + groups, total, err := a.service.GetTopErrorGroups(ctx, req) + if err != nil { + httputil.ProcessError(wr, err) + return + } + + wr.WriteJson(getTopGroupsResponse{ + Total: total, + Groups: newTopGroups(groups), + }) +} + +type getTopGroupsRequest struct { + Env *string `json:"env,omitempty"` + Source *string `json:"source,omitempty"` + // In go duration format. If not specified, then for the entire time. + Duration *string `json:"duration,omitempty" format:"duration" example:"1h"` + Limit uint32 `json:"limit"` + Offset uint32 `json:"offset"` + WithTotal bool `json:"with_total"` +} // @name errorgroups.v1.GetTopGroupsRequest + +type getTopGroupsResponse struct { + Total uint64 `json:"total"` + Groups []topGroup `json:"groups"` +} // @name errorgroups.v1.GetTopGroupsResponse + +type topGroup struct { + Hash string `json:"hash" format:"uint64"` + Message string `json:"message"` + Source string `json:"source"` + SeenTotal uint64 `json:"seen_total"` +} // @name errorgroups.v1.TopGroup + +func newTopGroups(source []types.TopErrorGroup) []topGroup { + groups := make([]topGroup, 0, len(source)) + + for _, g := range source { + groups = append(groups, topGroup{ + Hash: strconv.FormatUint(g.Hash, 10), + Message: g.Message, + Source: g.Source, + SeenTotal: g.Count, + }) + } + + return groups +} diff --git a/internal/api/errorgroups/v1/http/get_top_groups_test.go b/internal/api/errorgroups/v1/http/get_top_groups_test.go new file mode 100644 index 0000000..5f840cc --- /dev/null +++ b/internal/api/errorgroups/v1/http/get_top_groups_test.go @@ -0,0 +1,142 @@ +package http + +import ( + "errors" + "net/http" + "testing" + "time" + + "go.uber.org/mock/gomock" + + "github.com/ozontech/seq-ui/internal/api/httputil" + "github.com/ozontech/seq-ui/internal/app/types" + svc_mock "github.com/ozontech/seq-ui/internal/pkg/service/errorgroups/mock" +) + +func TestServeGetTopGroups(t *testing.T) { + var ( + env = "test-env" + source = "test-source" + durationStr = "2m" + duration = 2 * time.Minute + someErr = errors.New("some err") + ) + + type mockArgs struct { + req types.GetTopErrorGroupsRequest + + groups []types.TopErrorGroup + total uint64 + err error + } + + tests := []struct { + name string + + req getTopGroupsRequest + want getTopGroupsResponse + wantErr bool + + mockArgs *mockArgs + }{ + { + name: "ok", + + req: getTopGroupsRequest{ + Env: &env, + Source: &source, + Duration: &durationStr, + Limit: 2, + Offset: 0, + WithTotal: true, + }, + want: getTopGroupsResponse{ + Total: 10, + Groups: []topGroup{ + { + Hash: "123", + Message: "some error 1", + Source: source, + SeenTotal: 5, + }, + { + Hash: "456", + Message: "some error 2", + Source: source, + SeenTotal: 10, + }, + }, + }, + + mockArgs: &mockArgs{ + req: types.GetTopErrorGroupsRequest{ + Env: &env, + Source: &source, + Duration: &duration, + Limit: 2, + Offset: 0, + WithTotal: true, + }, + + groups: []types.TopErrorGroup{ + { + Hash: 123, + Message: "some error 1", + Source: source, + Count: 5, + }, + { + Hash: 456, + Message: "some error 2", + Source: source, + Count: 10, + }, + }, + total: 10, + }, + }, + + { + name: "err_svc", + + req: getTopGroupsRequest{}, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetTopErrorGroupsRequest{}, + + err: someErr, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockedSvc := svc_mock.NewMockService(ctrl) + + api := New(mockedSvc) + + if ma := tt.mockArgs; ma != nil { + mockedSvc.EXPECT(). + GetTopErrorGroups(gomock.Any(), ma.req). + Return(ma.groups, ma.total, ma.err). + Times(1) + } + + httputil.DoTestHTTPEx(t, httputil.TestDataHTTPEx[getTopGroupsRequest, getTopGroupsResponse]{ + Method: http.MethodPost, + Target: "/errorgroups/v1/top_groups", + Req: tt.req, + + Handler: api.serveGetTopGroups, + + Want: tt.want, + WantErr: tt.wantErr, + }) + }) + } +} diff --git a/internal/api/httputil/test_data.go b/internal/api/httputil/test_data.go index 9573e5e..2d957ee 100644 --- a/internal/api/httputil/test_data.go +++ b/internal/api/httputil/test_data.go @@ -1,6 +1,8 @@ package httputil import ( + "bytes" + "encoding/json" "io" "net/http" "net/http/httptest" @@ -36,3 +38,43 @@ func DoTestHTTP(t *testing.T, data TestDataHTTP) { require.Equal(t, data.WantRespBody, string(respBody)) } } + +type TestDataHTTPEx[Treq, Tresp any] struct { + Method string + Target string + Req Treq + + Handler http.HandlerFunc + + Want Tresp + NoResp bool + WantErr bool +} + +func DoTestHTTPEx[Treq, Tresp any](t *testing.T, data TestDataHTTPEx[Treq, Tresp]) { + w := httptest.NewRecorder() + + reqBody, err := json.Marshal(data.Req) + require.NoError(t, err) + req := httptest.NewRequest(data.Method, data.Target, bytes.NewReader(reqBody)) + + data.Handler(w, req) + + resp := w.Result() + defer func() { + _ = resp.Body.Close() + }() + + require.Equal(t, data.WantErr, resp.StatusCode != http.StatusOK) + if data.WantErr || data.NoResp { + return + } + + respBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + want, err := json.Marshal(data.Want) + require.NoError(t, err) + + require.Equal(t, want, respBody) +} diff --git a/internal/app/config/config.go b/internal/app/config/config.go index 62a4184..6872cad 100644 --- a/internal/app/config/config.go +++ b/internal/app/config/config.go @@ -304,8 +304,9 @@ type FieldFilterSet struct { } type LogTagsMapping struct { - Release []string `yaml:"release"` Env []string `yaml:"env"` + Service []string `yaml:"service"` + Release []string `yaml:"release"` } type ErrorGroups struct { diff --git a/internal/app/types/errorgroups.go b/internal/app/types/errorgroups.go index 959505e..012f4f8 100644 --- a/internal/app/types/errorgroups.go +++ b/internal/app/types/errorgroups.go @@ -27,15 +27,31 @@ type GetErrorGroupsRequest struct { type ErrorGroup struct { Hash uint64 Message string - SeenTotal uint64 + Count uint64 FirstSeenAt time.Time LastSeenAt time.Time Source string } +type GetTopErrorGroupsRequest struct { + Env *string + Source *string + Duration *time.Duration + Limit uint32 + Offset uint32 + WithTotal bool +} + +type TopErrorGroup struct { + Hash uint64 + Source string + Message string + Count uint64 +} + type GetErrorHistRequest struct { - Service string GroupHash *uint64 + Service *string Env *string Source *string Release *string @@ -48,20 +64,22 @@ type ErrorHistBucket struct { } type GetErrorGroupDetailsRequest struct { - Service string GroupHash uint64 Env *string Source *string + Service *string Release *string } func (r GetErrorGroupDetailsRequest) IsFullyFilled() bool { return r.Env != nil && *r.Env != "" && - r.Release != nil && *r.Release != "" + r.Release != nil && *r.Release != "" && + r.Service != nil && *r.Service != "" && + r.Source != nil && *r.Source != "" } type ErrorGroupDetails struct { - GroupHash uint64 + Hash uint64 Message string SeenTotal uint64 FirstSeenAt time.Time @@ -78,6 +96,8 @@ type ErrorGroupDistribution struct { type ErrorGroupDistributions struct { ByEnv []ErrorGroupDistribution + BySource []ErrorGroupDistribution + ByService []ErrorGroupDistribution ByRelease []ErrorGroupDistribution } @@ -85,14 +105,11 @@ type ErrorGroupCount map[string]uint64 type ErrorGroupCounts struct { ByEnv ErrorGroupCount + BySource ErrorGroupCount + ByService ErrorGroupCount ByRelease ErrorGroupCount } -type GetErrorGroupReleasesRequest struct { - Service string - Env *string -} - type GetServicesRequest struct { Query string Env *string @@ -100,6 +117,11 @@ type GetServicesRequest struct { Offset uint32 } +type GetReleasesRequest struct { + Service string + Env *string +} + type DiffByReleasesRequest struct { Service string Releases []string diff --git a/internal/pkg/repository/async_searches_repo.go b/internal/pkg/repository/async_searches_repo.go index 1ae43ba..8594134 100644 --- a/internal/pkg/repository/async_searches_repo.go +++ b/internal/pkg/repository/async_searches_repo.go @@ -5,8 +5,8 @@ import ( "errors" "fmt" - sq "github.com/Masterminds/squirrel" "github.com/jackc/pgx/v5" + sq "github.com/n-r-w/squirrel" "github.com/ozontech/seq-ui/internal/app/types" sqlb "github.com/ozontech/seq-ui/internal/pkg/repository/sql_builder" diff --git a/internal/pkg/repository/dashboards_repo.go b/internal/pkg/repository/dashboards_repo.go index 96a67f6..ade2afc 100644 --- a/internal/pkg/repository/dashboards_repo.go +++ b/internal/pkg/repository/dashboards_repo.go @@ -6,9 +6,9 @@ import ( "fmt" "strings" - sq "github.com/Masterminds/squirrel" "github.com/gofrs/uuid" "github.com/jackc/pgx/v5" + sq "github.com/n-r-w/squirrel" "github.com/ozontech/seq-ui/internal/app/types" sqlb "github.com/ozontech/seq-ui/internal/pkg/repository/sql_builder" ) diff --git a/internal/pkg/repository/sql_builder/builder.go b/internal/pkg/repository/sql_builder/builder.go index 097e76b..0a33967 100644 --- a/internal/pkg/repository/sql_builder/builder.go +++ b/internal/pkg/repository/sql_builder/builder.go @@ -1,6 +1,6 @@ package sql_builder -import sq "github.com/Masterminds/squirrel" +import sq "github.com/n-r-w/squirrel" func Select(columns ...string) sq.SelectBuilder { return sq.StatementBuilder.PlaceholderFormat(sq.Dollar).Select(columns...) diff --git a/internal/pkg/repository/user_profiles_repo.go b/internal/pkg/repository/user_profiles_repo.go index c8bae85..19c075c 100644 --- a/internal/pkg/repository/user_profiles_repo.go +++ b/internal/pkg/repository/user_profiles_repo.go @@ -6,8 +6,8 @@ import ( "errors" "fmt" - sq "github.com/Masterminds/squirrel" "github.com/jackc/pgx/v5" + sq "github.com/n-r-w/squirrel" "github.com/ozontech/seq-ui/internal/app/types" sqlb "github.com/ozontech/seq-ui/internal/pkg/repository/sql_builder" ) diff --git a/internal/pkg/repository_ch/error_groups.go b/internal/pkg/repository_ch/error_groups.go index 22ad1c5..bdf3e24 100644 --- a/internal/pkg/repository_ch/error_groups.go +++ b/internal/pkg/repository_ch/error_groups.go @@ -1,3 +1,4 @@ +// nolint:goconst package repositorych import ( @@ -5,11 +6,9 @@ import ( "database/sql" "errors" "fmt" - "maps" - "slices" "time" - sq "github.com/Masterminds/squirrel" + sq "github.com/n-r-w/squirrel" "github.com/ozontech/seq-ui/internal/app/types" ) @@ -18,46 +17,236 @@ func (r *repository) GetErrorGroups( ctx context.Context, req types.GetErrorGroupsRequest, ) ([]types.ErrorGroup, error) { - // we need this subquery to make query faster, see https://github.com/ClickHouse/ClickHouse/issues/7187 - subQ := sq. - Select("_group_hash"). - From("error_groups"). - Where(sq.Eq{"service": req.Service}). - GroupBy("_group_hash", "service"). - Limit(uint64(req.Limit)). - Offset(uint64(req.Offset)) - - if r.sharded { - subQ = subQ.Distinct() + where := sq.Eq{ + "service": req.Service, } - for col, val := range r.queryFilters() { - subQ = subQ.Where(sq.Eq{col: val}).GroupBy(col) + where[col] = val } - if req.Env != nil && *req.Env != "" { - subQ = subQ.Where(sq.Eq{"env": req.Env}).GroupBy("env") + where["env"] = *req.Env + } + if req.Source != nil && *req.Source != "" { + where["source"] = *req.Source } if req.Release != nil && *req.Release != "" { - subQ = subQ.Where(sq.Eq{"release": req.Release}).GroupBy("release") + where["release"] = *req.Release } + if req.Duration != nil && *req.Duration != 0 { - subQ = subQ.Having(sq.GtOrEq{"maxMerge(last_seen_at)": r.nowFn().Add(-req.Duration.Abs())}) - } - if req.Source != nil && *req.Source != "" { - subQ = subQ.Where(sq.Eq{"source": req.Source}).GroupBy("source") + aggTable, startDate := getHistData(req.Duration) + + if req.Order == types.OrderFrequent { + countQ := sq. + Select( + "_group_hash", + "countMerge(counts) as count", + ). + From(aggTable). + Where(where). + Where(sq.GtOrEq{startDate: r.nowFn().Add(-req.Duration.Abs())}). + GroupBy("_group_hash"). + OrderBy("count DESC"). + Limit(uint64(req.Limit)). + Offset(uint64(req.Offset)) + + countQuery, countArgs := countQ.MustSql() + metricLabels := []string{aggTable, "SELECT"} + countRows, err := r.conn.Query(ctx, metricLabels, countQuery, countArgs...) + if err != nil { + incErrorMetric(err, metricLabels) + return nil, fmt.Errorf("failed to get error groups count: %w", err) + } + + var ( + groups []types.ErrorGroup + idxByHash = map[uint64]int{} + hashes []uint64 + ) + for countRows.Next() { + var group types.ErrorGroup + if err = countRows.Scan( + &group.Hash, + &group.Count, + ); err != nil { + return nil, fmt.Errorf("failed to scan row: %w", err) + } + + groups = append(groups, group) + hashes = append(hashes, group.Hash) + idxByHash[group.Hash] = len(groups) - 1 + } + + if len(groups) == 0 { + return nil, nil + } + + where["_group_hash"] = hashes + q := sq. + Select( + "_group_hash", + "source", + "any(message) as message", + "minMerge(first_seen_at) as first_seen_at", + "maxMerge(last_seen_at) as last_seen_at", + ). + From("error_groups"). + Where(where). + GroupBy("_group_hash", "source") + + query, args := q.MustSql() + metricLabels = []string{"error_groups", "SELECT"} + rows, err := r.conn.Query(ctx, metricLabels, query, args...) + if err != nil { + incErrorMetric(err, metricLabels) + return nil, fmt.Errorf("failed to get error groups: %w", err) + } + + var ( + hash uint64 + message, source string + firstSeen, lastSeen time.Time + ) + for rows.Next() { + if err = rows.Scan( + &hash, + &source, + &message, + &firstSeen, + &lastSeen, + ); err != nil { + return nil, fmt.Errorf("failed to scan row: %w", err) + } + + idx := idxByHash[hash] + groups[idx].Source = source + groups[idx].Message = message + groups[idx].FirstSeenAt = firstSeen + groups[idx].LastSeenAt = lastSeen + } + + return groups, nil + } + + subQ := sq. + Select("_group_hash"). + From("error_groups"). + Where(where). + Having(sq.GtOrEq{"maxMerge(last_seen_at)": r.nowFn().Add(-req.Duration.Abs())}). + GroupBy("_group_hash"). + OrderBy(orderBy(req.Order, true)). + Limit(uint64(req.Limit)). + Offset(uint64(req.Offset)) + + in := "IN" + if r.sharded { + subQ = subQ.Distinct() + in = "GLOBAL IN" + } + + subQuery, subArgs := subQ.MustSql() + + q := sq. + Select( + "_group_hash", + "source", + "any(message) as message", + "minMerge(first_seen_at) as first_seen_at", + "maxMerge(last_seen_at) as last_seen_at", + ). + From("error_groups"). + Where(where). + Where(fmt.Sprintf("_group_hash %s (%s)", in, subQuery), subArgs...). + GroupBy("_group_hash", "source"). + OrderBy(orderBy(req.Order, false)) + + query, args := q.MustSql() + metricLabels := []string{"error_groups", "SELECT"} + rows, err := r.conn.Query(ctx, metricLabels, query, args...) + if err != nil { + incErrorMetric(err, metricLabels) + return nil, fmt.Errorf("failed to get error groups: %w", err) + } + + var ( + groups []types.ErrorGroup + idxByHash = map[uint64]int{} + hashes []uint64 + ) + for rows.Next() { + var group types.ErrorGroup + if err = rows.Scan( + &group.Hash, + &group.Source, + &group.Message, + &group.FirstSeenAt, + &group.LastSeenAt, + ); err != nil { + return nil, fmt.Errorf("failed to scan row: %w", err) + } + + groups = append(groups, group) + hashes = append(hashes, group.Hash) + idxByHash[group.Hash] = len(groups) - 1 + } + + if len(groups) == 0 { + return nil, nil + } + + where["_group_hash"] = hashes + countQ := sq. + Select( + "_group_hash", + "countMerge(counts) as count", + ). + From(aggTable). + Where(where). + GroupBy("_group_hash") + + countQuery, countArgs := countQ.MustSql() + metricLabels = []string{aggTable, "SELECT"} + countRows, err := r.conn.Query(ctx, metricLabels, countQuery, countArgs...) + if err != nil { + incErrorMetric(err, metricLabels) + return nil, fmt.Errorf("failed to get error groups count: %w", err) + } + + var hash, count uint64 + for countRows.Next() { + if err = countRows.Scan( + &hash, + &count, + ); err != nil { + return nil, fmt.Errorf("failed to scan row: %w", err) + } + + groups[idxByHash[hash]].Count = count + } + + return groups, nil } - subQ = orderBy(subQ, req.Order, true) - subQuery, subArgs := subQ.MustSql() + subQ := sq. + Select("_group_hash"). + From("error_groups"). + Where(where). + GroupBy("_group_hash"). + OrderBy(orderBy(req.Order, true)). + Limit(uint64(req.Limit)). + Offset(uint64(req.Offset)) in := "IN" if r.sharded { + subQ = subQ.Distinct() in = "GLOBAL IN" } + + subQuery, subArgs := subQ.MustSql() + q := sq. Select( - "_group_hash as group_hash", + "_group_hash", "source", "any(message) as message", "countMerge(seen_total) as seen_total", @@ -65,26 +254,10 @@ func (r *repository) GetErrorGroups( "maxMerge(last_seen_at) as last_seen_at", ). From("error_groups"). + Where(where). Where(fmt.Sprintf("_group_hash %s (%s)", in, subQuery), subArgs...). - GroupBy("_group_hash", "service", "source") - - // using string formatting below because squirrel doesn't support subquery in WHERE clause - q = q.Where(fmt.Sprintf("service = '%s'", req.Service)) - - for col, val := range r.queryFilters() { - q = q.Where(fmt.Sprintf("%s = '%s'", col, val)).GroupBy(col) - } - - if req.Source != nil && *req.Source != "" { - q = q.Where(fmt.Sprintf("source = '%s'", *req.Source)) - } - if req.Env != nil && *req.Env != "" { - q = q.Where(fmt.Sprintf("env = '%s'", *req.Env)).GroupBy("env") - } - if req.Release != nil && *req.Release != "" { - q = q.Where(fmt.Sprintf("release = '%s'", *req.Release)).GroupBy("release") - } - q = orderBy(q, req.Order, false) + GroupBy("_group_hash", "source"). + OrderBy(orderBy(req.Order, false)) query, args := q.MustSql() metricLabels := []string{"error_groups", "SELECT"} @@ -94,28 +267,27 @@ func (r *repository) GetErrorGroups( return nil, fmt.Errorf("failed to get error groups: %w", err) } - var errorGroups []types.ErrorGroup + var groups []types.ErrorGroup for rows.Next() { var group types.ErrorGroup - err = rows.Scan( + if err = rows.Scan( &group.Hash, &group.Source, &group.Message, - &group.SeenTotal, + &group.Count, &group.FirstSeenAt, &group.LastSeenAt, - ) - if err != nil { + ); err != nil { return nil, fmt.Errorf("failed to scan row: %w", err) } - errorGroups = append(errorGroups, group) + groups = append(groups, group) } - return errorGroups, nil + return groups, nil } -func (r *repository) GetErrorGroupsCount( +func (r *repository) GetErrorGroupsTotal( ctx context.Context, req types.GetErrorGroupsRequest, ) (uint64, error) { @@ -123,24 +295,23 @@ func (r *repository) GetErrorGroupsCount( Select("maxMerge(last_seen_at) AS last_seen_at"). From("error_groups"). Where(sq.Eq{"service": req.Service}). - GroupBy("_group_hash", "service") + GroupBy("_group_hash") for col, val := range r.queryFilters() { - subQ = subQ.Where(sq.Eq{col: val}).GroupBy(col) + subQ = subQ.Where(sq.Eq{col: val}) } - if req.Env != nil && *req.Env != "" { - subQ = subQ.Where(sq.Eq{"env": req.Env}).GroupBy("env") + subQ = subQ.Where(sq.Eq{"env": *req.Env}) + } + if req.Source != nil && *req.Source != "" { + subQ = subQ.Where(sq.Eq{"source": *req.Source}) } if req.Release != nil && *req.Release != "" { - subQ = subQ.Where(sq.Eq{"release": req.Release}).GroupBy("release") + subQ = subQ.Where(sq.Eq{"release": *req.Release}) } if req.Duration != nil && *req.Duration != 0 { subQ = subQ.Having(sq.GtOrEq{"last_seen_at": r.nowFn().Add(-req.Duration.Abs())}) } - if req.Source != nil && *req.Source != "" { - subQ = subQ.Where(sq.Eq{"source": req.Source}).GroupBy("source") - } q := sq.Select("count()").FromSelect(subQ, "subQ") @@ -164,45 +335,45 @@ func (r *repository) GetNewErrorGroups( ctx context.Context, req types.GetErrorGroupsRequest, ) ([]types.ErrorGroup, error) { - // we need this subquery to make query faster, see https://github.com/ClickHouse/ClickHouse/issues/7187 + where := sq.Eq{ + "service": req.Service, + } + for col, val := range r.queryFilters() { + where[col] = val + } + if req.Env != nil && *req.Env != "" { + where["env"] = *req.Env + } + if req.Source != nil && *req.Source != "" { + where["source"] = *req.Source + } + subQ := sq. Select("_group_hash"). From("error_groups"). - Where(sq.Eq{"service": req.Service}). - GroupBy("_group_hash", "source"). + Where(where). + GroupBy("_group_hash"). + OrderBy(orderBy(req.Order, true)). Limit(uint64(req.Limit)). Offset(uint64(req.Offset)) + in := "IN" if r.sharded { subQ = subQ.Distinct() - } - for col, val := range r.queryFilters() { - subQ = subQ.Where(sq.Eq{col: val}) - } - if req.Env != nil && *req.Env != "" { - subQ = subQ.Where(sq.Eq{"env": req.Env}) - } - if req.Source != nil && *req.Source != "" { - subQ = subQ.Where(sq.Eq{"source": req.Source}) + in = "GLOBAL IN" } if req.Release != nil && *req.Release != "" { // new by releases, ignore duration subQ = subQ.Having(sq.Eq{ - "count()": 1, "any(release)": *req.Release, + "count()": 1, }) } else if req.Duration != nil && *req.Duration != 0 { // new by duration subQ = subQ.Having(sq.GtOrEq{"minMerge(first_seen_at)": r.nowFn().Add(-req.Duration.Abs())}) } - subQ = orderBy(subQ, req.Order, true) - subQuery, subArgs := subQ.MustSql() - in := "IN" - if r.sharded { - in = "GLOBAL IN" - } q := sq. Select( "_group_hash", @@ -213,23 +384,10 @@ func (r *repository) GetNewErrorGroups( "maxMerge(last_seen_at) as last_seen_at", ). From("error_groups"). + Where(where). Where(fmt.Sprintf("_group_hash %s (%s)", in, subQuery), subArgs...). - GroupBy("_group_hash", "source") - - // using string formatting below because squirrel doesn't support subquery in WHERE clause - q = q.Where(fmt.Sprintf("service = '%s'", req.Service)) - - for col, val := range r.queryFilters() { - q = q.Where(fmt.Sprintf("%s = '%s'", col, val)) - } - - if req.Source != nil && *req.Source != "" { - q = q.Where(fmt.Sprintf("source = '%s'", *req.Source)) - } - if req.Env != nil && *req.Env != "" { - q = q.Where(fmt.Sprintf("env = '%s'", *req.Env)) - } - q = orderBy(q, req.Order, false) + GroupBy("_group_hash", "source"). + OrderBy(orderBy(req.Order, false)) query, args := q.MustSql() metricLabels := []string{"error_groups", "SELECT"} @@ -239,28 +397,27 @@ func (r *repository) GetNewErrorGroups( return nil, fmt.Errorf("failed to get new error groups: %w", err) } - var errorGroups []types.ErrorGroup + var groups []types.ErrorGroup for rows.Next() { var group types.ErrorGroup - err = rows.Scan( + if err = rows.Scan( &group.Hash, &group.Source, &group.Message, - &group.SeenTotal, + &group.Count, &group.FirstSeenAt, &group.LastSeenAt, - ) - if err != nil { + ); err != nil { return nil, fmt.Errorf("failed to scan row: %w", err) } - errorGroups = append(errorGroups, group) + groups = append(groups, group) } - return errorGroups, nil + return groups, nil } -func (r *repository) GetNewErrorGroupsCount( +func (r *repository) GetNewErrorGroupsTotal( ctx context.Context, req types.GetErrorGroupsRequest, ) (uint64, error) { @@ -268,22 +425,22 @@ func (r *repository) GetNewErrorGroupsCount( Select("_group_hash"). From("error_groups"). Where(sq.Eq{"service": req.Service}). - GroupBy("_group_hash", "source") + GroupBy("_group_hash") for col, val := range r.queryFilters() { subQ = subQ.Where(sq.Eq{col: val}) } if req.Env != nil && *req.Env != "" { - subQ = subQ.Where(sq.Eq{"env": req.Env}) + subQ = subQ.Where(sq.Eq{"env": *req.Env}) } if req.Source != nil && *req.Source != "" { - subQ = subQ.Where(sq.Eq{"source": req.Source}) + subQ = subQ.Where(sq.Eq{"source": *req.Source}) } if req.Release != nil && *req.Release != "" { // new by releases, ignore duration subQ = subQ.Having(sq.Eq{ - "count()": 1, "any(release)": *req.Release, + "count()": 1, }) } else if req.Duration != nil && *req.Duration != 0 { // new by duration subQ = subQ.Having(sq.GtOrEq{"minMerge(first_seen_at)": r.nowFn().Add(-req.Duration.Abs())}) @@ -307,41 +464,242 @@ func (r *repository) GetNewErrorGroupsCount( return total, nil } +func (r *repository) GetTopErrorGroups( + ctx context.Context, + req types.GetTopErrorGroupsRequest, +) ([]types.TopErrorGroup, error) { + where := sq.Eq{} + for col, val := range r.queryFilters() { + where[col] = val + } + if req.Env != nil && *req.Env != "" { + where["env"] = *req.Env + } + if req.Source != nil && *req.Source != "" { + where["source"] = *req.Source + } + + if req.Duration != nil && *req.Duration != 0 { + aggTable, startDate := getHistData(req.Duration) + + countQ := sq. + Select( + "_group_hash", + "countMerge(counts) as count", + ). + From(aggTable). + Where(where). + Where(sq.GtOrEq{startDate: r.nowFn().Add(-req.Duration.Abs())}). + GroupBy("_group_hash"). + OrderBy("count DESC"). + Limit(uint64(req.Limit)). + Offset(uint64(req.Offset)) + + countQuery, countArgs := countQ.MustSql() + metricLabels := []string{aggTable, "SELECT"} + countRows, err := r.conn.Query(ctx, metricLabels, countQuery, countArgs...) + if err != nil { + incErrorMetric(err, metricLabels) + return nil, fmt.Errorf("failed to get error groups count: %w", err) + } + + var ( + groups []types.TopErrorGroup + idxByHash = map[uint64]int{} + hashes []uint64 + ) + for countRows.Next() { + var group types.TopErrorGroup + if err = countRows.Scan( + &group.Hash, + &group.Count, + ); err != nil { + return nil, fmt.Errorf("failed to scan row: %w", err) + } + + groups = append(groups, group) + hashes = append(hashes, group.Hash) + idxByHash[group.Hash] = len(groups) - 1 + } + + if len(groups) == 0 { + return nil, nil + } + + where["_group_hash"] = hashes + q := sq. + Select( + "_group_hash", + "source", + "any(message) as message", + ). + From("error_groups"). + Where(where). + GroupBy("_group_hash", "source") + + query, args := q.MustSql() + metricLabels = []string{"error_groups", "SELECT"} + rows, err := r.conn.Query(ctx, metricLabels, query, args...) + if err != nil { + incErrorMetric(err, metricLabels) + return nil, fmt.Errorf("failed to get error groups: %w", err) + } + + var ( + hash uint64 + message, source string + ) + for rows.Next() { + if err = rows.Scan( + &hash, + &source, + &message, + ); err != nil { + return nil, fmt.Errorf("failed to scan row: %w", err) + } + + idx := idxByHash[hash] + groups[idx].Message = message + groups[idx].Source = source + } + + return groups, nil + } + + subQ := sq. + Select("_group_hash"). + From("error_groups_brief"). + Where(where). + GroupBy("_group_hash"). + OrderBy(orderBy(types.OrderFrequent, true)). + Limit(uint64(req.Limit)). + Offset(uint64(req.Offset)) + + in := "IN" + if r.sharded { + subQ = subQ.Distinct() + in = "GLOBAL IN" + } + + subQuery, subArgs := subQ.MustSql() + + q := sq. + Select( + "_group_hash", + "source", + "any(message) as message", + "countMerge(seen_total) as seen_total", + ). + From("error_groups"). + Where(where). + Where(fmt.Sprintf("_group_hash %s (%s)", in, subQuery), subArgs...). + GroupBy("_group_hash", "source"). + OrderBy(orderBy(types.OrderFrequent, false)) + + query, args := q.MustSql() + metricLabels := []string{"error_groups", "SELECT"} + rows, err := r.conn.Query(ctx, metricLabels, query, args...) + if err != nil { + incErrorMetric(err, metricLabels) + return nil, fmt.Errorf("failed to get error groups: %w", err) + } + + var groups []types.TopErrorGroup + for rows.Next() { + var group types.TopErrorGroup + if err = rows.Scan( + &group.Hash, + &group.Source, + &group.Message, + &group.Count, + ); err != nil { + return nil, fmt.Errorf("failed to scan row: %w", err) + } + + groups = append(groups, group) + } + + return groups, nil +} + +func (r *repository) GetTopErrorGroupsTotal( + ctx context.Context, + req types.GetTopErrorGroupsRequest, +) (uint64, error) { + q := sq.Select("uniq(_group_hash)") + + for col, val := range r.queryFilters() { + q = q.Where(sq.Eq{col: val}) + } + if req.Env != nil && *req.Env != "" { + q = q.Where(sq.Eq{"env": *req.Env}) + } + if req.Source != nil && *req.Source != "" { + q = q.Where(sq.Eq{"source": *req.Source}) + } + + var table string + if req.Duration != nil && *req.Duration != 0 { + aggTable, startDate := getHistData(req.Duration) + q = q.Where(sq.GtOrEq{startDate: r.nowFn().Add(-req.Duration.Abs())}) + + table = aggTable + } else { + table = "error_groups_brief" + } + + query, args := q.From(table).MustSql() + metricLabels := []string{table, "SELECT"} + row := r.conn.QueryRow(ctx, metricLabels, query, args...) + + var total uint64 + if err := row.Scan(&total); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return 0, nil + } + incErrorMetric(err, metricLabels) + return 0, fmt.Errorf("failed to get top error groups count: %w", err) + } + + return total, nil +} + func (r *repository) GetErrorHist( ctx context.Context, req types.GetErrorHistRequest, ) ([]types.ErrorHistBucket, error) { - startDate := getHistBucketSize(req.Duration) + table, startDate := getHistData(req.Duration) q := sq. Select( startDate, "countMerge(counts) as counts", ). - From("agg_events_10min"). - Where(sq.Eq{"service": req.Service}). - GroupBy(startDate, "service"). + From(table). + GroupBy(startDate). OrderBy(startDate) for col, val := range r.queryFilters() { - q = q.Where(sq.Eq{col: val}).GroupBy(col) + q = q.Where(sq.Eq{col: val}) } - if req.GroupHash != nil && *req.GroupHash != 0 { - q = q.Where(sq.Eq{"_group_hash": req.GroupHash}).GroupBy("_group_hash") + q = q.Where(sq.Eq{"_group_hash": *req.GroupHash}) } if req.Env != nil && *req.Env != "" { - q = q.Where(sq.Eq{"env": req.Env}).GroupBy("env") + q = q.Where(sq.Eq{"env": *req.Env}) + } + if req.Source != nil && *req.Source != "" { + q = q.Where(sq.Eq{"source": *req.Source}) + } + if req.Service != nil && *req.Service != "" { + q = q.Where(sq.Eq{"service": *req.Service}) } if req.Release != nil && *req.Release != "" { - q = q.Where(sq.Eq{"release": req.Release}).GroupBy("release") + q = q.Where(sq.Eq{"release": *req.Release}) } if req.Duration != nil && *req.Duration != 0 { q = q.Where(sq.GtOrEq{startDate: r.nowFn().Add(-req.Duration.Abs())}) } - if req.Source != nil && *req.Source != "" { - q = q.Where(sq.Eq{"source": req.Source}).GroupBy("source") - } query, args := q.MustSql() metricLabels := []string{"agg_events_10min", "SELECT"} @@ -354,7 +712,10 @@ func (r *repository) GetErrorHist( var buckets []types.ErrorHistBucket for rows.Next() { var bucket types.ErrorHistBucket - if err := rows.Scan(&bucket.Time, &bucket.Count); err != nil { + if err := rows.Scan( + &bucket.Time, + &bucket.Count, + ); err != nil { return nil, fmt.Errorf("failed to scan row: %w", err) } buckets = append(buckets, bucket) @@ -369,7 +730,7 @@ func (r *repository) GetErrorDetails( ) (types.ErrorGroupDetails, error) { q := sq. Select( - "_group_hash as group_hash", + "_group_hash", "source", "any(message) as message", "countMerge(seen_total) as seen_total", @@ -378,41 +739,39 @@ func (r *repository) GetErrorDetails( "max(log_tags) as log_tags", ). From("error_groups"). - Where(sq.Eq{ - "service": req.Service, - "_group_hash": req.GroupHash, - }). - GroupBy("_group_hash", "service", "source") + Where(sq.Eq{"_group_hash": req.GroupHash}). + GroupBy("_group_hash", "source") for col, val := range r.queryFilters() { - q = q.Where(sq.Eq{col: val}).GroupBy(col) + q = q.Where(sq.Eq{col: val}) } - if req.Env != nil && *req.Env != "" { - q = q.Where(sq.Eq{"env": req.Env}).GroupBy("env") - } - if req.Release != nil && *req.Release != "" { - q = q.Where(sq.Eq{"release": req.Release}).GroupBy("release") + q = q.Where(sq.Eq{"env": *req.Env}) } if req.Source != nil && *req.Source != "" { - q = q.Where(sq.Eq{"source": req.Source}) + q = q.Where(sq.Eq{"source": *req.Source}) + } + if req.Service != nil && *req.Service != "" { + q = q.Where(sq.Eq{"service": *req.Service}) + } + if req.Release != nil && *req.Release != "" { + q = q.Where(sq.Eq{"release": *req.Release}) } - - var details types.ErrorGroupDetails query, args := q.MustSql() metricLabels := []string{"error_groups", "SELECT"} row := r.conn.QueryRow(ctx, metricLabels, query, args...) - err := row.Scan( - &details.GroupHash, + + var details types.ErrorGroupDetails + if err := row.Scan( + &details.Hash, &details.Source, &details.Message, &details.SeenTotal, &details.FirstSeenAt, &details.LastSeenAt, &details.LogTags, - ) - if err != nil && !errors.Is(err, sql.ErrNoRows) { + ); err != nil && !errors.Is(err, sql.ErrNoRows) { incErrorMetric(err, metricLabels) return details, fmt.Errorf("failed to get error details: %w", err) } @@ -420,37 +779,56 @@ func (r *repository) GetErrorDetails( return details, nil } +type errCounts struct { + count uint64 + env string + source string + service string + release string +} + func (r *repository) GetErrorCounts( ctx context.Context, req types.GetErrorGroupDetailsRequest, ) (types.ErrorGroupCounts, error) { counts := types.ErrorGroupCounts{ ByEnv: types.ErrorGroupCount{}, + BySource: types.ErrorGroupCount{}, + ByService: types.ErrorGroupCount{}, ByRelease: types.ErrorGroupCount{}, } q := sq. - Select("countMerge(seen_total) as seen_total", "env", "release"). + Select( + "countMerge(seen_total) as count", + "env", + "source", + "service", + ). From("error_groups"). - Where(sq.Eq{ - "service": req.Service, - "_group_hash": req.GroupHash, - }). - GroupBy("_group_hash", "service", "env", "release"). - OrderBy("seen_total DESC") + Where(sq.Eq{"_group_hash": req.GroupHash}). + GroupBy("env", "source", "service") for col, val := range r.queryFilters() { - q = q.Where(sq.Eq{col: val}).GroupBy(col) + q = q.Where(sq.Eq{col: val}) } - if req.Env != nil && *req.Env != "" { - q = q.Where(sq.Eq{"env": *req.Env}) - } - if req.Release != nil && *req.Release != "" { - q = q.Where(sq.Eq{"release": *req.Release}) + addFilter := func(col string, val *string) { + if val != nil && *val != "" { + q = q.Where(sq.Eq{col: *val}) + } } - if req.Source != nil && *req.Source != "" { - q = q.Where(sq.Eq{"source": *req.Source}) + + addFilter("env", req.Env) + addFilter("source", req.Source) + addFilter("service", req.Service) + + // releases only with service + withRelease := false + if req.Service != nil && *req.Service != "" { + withRelease = true + addFilter("release", req.Release) + q = q.Columns("release").GroupBy("release") } query, args := q.MustSql() @@ -461,40 +839,46 @@ func (r *repository) GetErrorCounts( return counts, fmt.Errorf("failed to get error counts: %w", err) } + var ec errCounts for rows.Next() { - var ( - seen uint64 - env, release string - ) - if err := rows.Scan(&seen, &env, &release); err != nil { + if err = rows.ScanStruct(&ec); err != nil { return counts, fmt.Errorf("failed to scan row: %w", err) } - counts.ByEnv[env] += seen - counts.ByRelease[release] += seen + counts.ByEnv[ec.env] += ec.count + counts.BySource[ec.source] += ec.count + counts.ByService[ec.service] += ec.count + if withRelease { + counts.ByRelease[ec.release] += ec.count + } } return counts, nil } -func (r *repository) GetErrorReleases( +func (r *repository) GetServices( ctx context.Context, - req types.GetErrorGroupReleasesRequest, + req types.GetServicesRequest, ) ([]string, error) { q := sq. - Select("release").Distinct(). + Select("service").Distinct(). From("services"). - Where(sq.And{ - sq.Eq{"service": req.Service}, - sq.NotEq{"release": ""}, - }). - OrderBy("ttl DESC") + Where(sq.NotEq{"service": ""}). + OrderBy("service") for col, val := range r.queryFilters() { q = q.Where(sq.Eq{col: val}) } - + if req.Query != "" { + q = q.Where("startsWith(service, ?)", req.Query) + } if req.Env != nil && *req.Env != "" { - q = q.Where(sq.Eq{"env": req.Env}) + q = q.Where(sq.Eq{"env": *req.Env}) + } + if req.Limit > 0 { + q = q.Limit(uint64(req.Limit)) + } + if req.Offset > 0 { + q = q.Offset(uint64(req.Offset)) } query, args := q.MustSql() @@ -502,48 +886,39 @@ func (r *repository) GetErrorReleases( rows, err := r.conn.Query(ctx, metricLabels, query, args...) if err != nil { incErrorMetric(err, metricLabels) - return nil, fmt.Errorf("failed to get releases: %w", err) + return nil, fmt.Errorf("failed to get services: %w", err) } - releases := make([]string, 0) + services := make([]string, 0) for rows.Next() { - var release string - if err := rows.Scan(&release); err != nil { + var service string + if err := rows.Scan(&service); err != nil { return nil, fmt.Errorf("failed to scan row: %w", err) } - if release == "" { - continue - } - releases = append(releases, release) + services = append(services, service) } - return releases, nil + return services, nil } -func (r *repository) GetServices( +func (r *repository) GetReleases( ctx context.Context, - req types.GetServicesRequest, + req types.GetReleasesRequest, ) ([]string, error) { q := sq. - Select("service").Distinct(). + Select("release").Distinct(). From("services"). - Where("startsWith(service, ?)", req.Query). - Where(sq.NotEq{"service": ""}). - OrderBy("service") + Where(sq.And{ + sq.Eq{"service": req.Service}, + sq.NotEq{"release": ""}, + }). + OrderBy("ttl DESC") for col, val := range r.queryFilters() { q = q.Where(sq.Eq{col: val}) } - if req.Env != nil && *req.Env != "" { - q = q.Where(sq.Eq{"env": req.Env}) - } - - if req.Limit > 0 { - q = q.Limit(uint64(req.Limit)) - } - if req.Offset > 0 { - q = q.Offset(uint64(req.Offset)) + q = q.Where(sq.Eq{"env": *req.Env}) } query, args := q.MustSql() @@ -551,19 +926,19 @@ func (r *repository) GetServices( rows, err := r.conn.Query(ctx, metricLabels, query, args...) if err != nil { incErrorMetric(err, metricLabels) - return nil, fmt.Errorf("failed to get services: %w", err) + return nil, fmt.Errorf("failed to get releases: %w", err) } - services := make([]string, 0) + releases := make([]string, 0) for rows.Next() { - var service string - if err := rows.Scan(&service); err != nil { + var release string + if err := rows.Scan(&release); err != nil { return nil, fmt.Errorf("failed to scan row: %w", err) } - services = append(services, service) + releases = append(releases, release) } - return services, nil + return releases, nil } func (r *repository) DiffByReleases( @@ -616,8 +991,9 @@ func (r *repository) DiffByReleases( } var ( - diffGroups []types.DiffGroup - idxByHash = map[uint64]int{} + groups []types.DiffGroup + idxByHash = map[uint64]int{} + hashes []uint64 ) for rows.Next() { group := types.DiffGroup{ @@ -627,29 +1003,26 @@ func (r *repository) DiffByReleases( group.ReleaseInfos[r] = types.DiffReleaseInfo{} } - err = rows.Scan( + if err = rows.Scan( &group.Hash, &group.Source, &group.Message, &group.FirstSeenAt, &group.LastSeenAt, - ) - if err != nil { + ); err != nil { return nil, fmt.Errorf("failed to scan row: %w", err) } - diffGroups = append(diffGroups, group) - idxByHash[group.Hash] = len(diffGroups) - 1 + groups = append(groups, group) + hashes = append(hashes, group.Hash) + idxByHash[group.Hash] = len(groups) - 1 } - if len(diffGroups) == 0 { + if len(groups) == 0 { return nil, nil } - hashes := slices.Collect(maps.Keys(idxByHash)) - slices.Sort(hashes) where["_group_hash"] = hashes - q := sq. Select( "_group_hash", @@ -672,18 +1045,20 @@ func (r *repository) DiffByReleases( hash, seenTotal uint64 release string ) - if err := rows.Scan(&hash, &release, &seenTotal); err != nil { + if err := rows.Scan( + &hash, + &release, + &seenTotal, + ); err != nil { return nil, fmt.Errorf("failed to scan row: %w", err) } - if idx, ok := idxByHash[hash]; ok { - diffGroups[idx].ReleaseInfos[release] = types.DiffReleaseInfo{ - SeenTotal: seenTotal, - } + groups[idxByHash[hash]].ReleaseInfos[release] = types.DiffReleaseInfo{ + SeenTotal: seenTotal, } } - return diffGroups, nil + return groups, nil } func (r *repository) DiffByReleasesTotal( @@ -727,7 +1102,7 @@ func (r *repository) DiffByReleasesTotal( return total, nil } -func orderBy(q sq.SelectBuilder, o types.ErrorGroupsOrder, sub bool) sq.SelectBuilder { +func orderBy(o types.ErrorGroupsOrder, sub bool) string { seenTotal := "seen_total DESC" lastSeenAt := "last_seen_at DESC" firstSeenAt := "first_seen_at" @@ -739,36 +1114,46 @@ func orderBy(q sq.SelectBuilder, o types.ErrorGroupsOrder, sub bool) sq.SelectBu switch o { case types.OrderFrequent: - q = q.OrderBy(seenTotal) + return seenTotal case types.OrderLatest: - q = q.OrderBy(lastSeenAt) + return lastSeenAt case types.OrderOldest: - q = q.OrderBy(firstSeenAt) + return firstSeenAt } - return q + return seenTotal } -func getHistBucketSize(d *time.Duration) string { +func getHistData(duration *time.Duration) (string, string) { const ( - startDate = "start_date" - startOfHour = "toStartOfHour(start_date)" - startOfDay = "toStartOfDay(start_date)" - day = 24 * time.Hour + table_10min = "agg_events_10min" + table_1d = "agg_events_1d" + + startDate = "start_date" + startOfHour = "toStartOfHour(start_date)" + startOfDay = "toStartOfDay(start_date)" + startOfWeek = "toStartOfWeek(start_date)" + startOfMonth = "toStartOfMonth(start_date)" + + day = 24 * time.Hour + month = 30 * day ) - if d == nil { - return startOfDay + if duration == nil || *duration == 0 { + return table_1d, startOfMonth } - duration := *d + // try get ~30 buckets + d := *duration switch { - case duration < 7*time.Hour: - return startDate - case duration < 7*day: - return startOfHour - case duration >= 7*day: - return startOfDay + case d <= 5*time.Hour: + return table_10min, startDate + case d <= day: + return table_10min, startOfHour + case d <= month: + return table_10min, startOfDay + case d <= 7*month: + return table_1d, startOfWeek default: - return startOfDay + return table_1d, startOfMonth } } diff --git a/internal/pkg/repository_ch/error_groups_test.go b/internal/pkg/repository_ch/error_groups_test.go index e759a0a..135af0f 100644 --- a/internal/pkg/repository_ch/error_groups_test.go +++ b/internal/pkg/repository_ch/error_groups_test.go @@ -9,18 +9,98 @@ import ( "time" "github.com/ozontech/seq-ui/internal/app/types" - mock "github.com/ozontech/seq-ui/internal/pkg/repository_ch/mock" "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" ) -func fakeNow(now time.Time) func() time.Time { - return func() time.Time { - return now +func TestGetHistData(t *testing.T) { + const ( + day = 24 * time.Hour + month = 30 * day + ) + + tests := []struct { + name string + + duration time.Duration + + wantTable, wantColumn string + }{ + { + name: "nil", + + duration: -1, + + wantTable: "agg_events_1d", + wantColumn: "toStartOfMonth(start_date)", + }, + { + name: "zero", + + duration: 0, + + wantTable: "agg_events_1d", + wantColumn: "toStartOfMonth(start_date)", + }, + { + name: "5_hour", + + duration: 5 * time.Hour, + + wantTable: "agg_events_10min", + wantColumn: "start_date", + }, + { + name: "1_day", + + duration: day, + + wantTable: "agg_events_10min", + wantColumn: "toStartOfHour(start_date)", + }, + { + name: "1_month", + + duration: month, + + wantTable: "agg_events_10min", + wantColumn: "toStartOfDay(start_date)", + }, + { + name: "7_month", + + duration: 7 * month, + + wantTable: "agg_events_1d", + wantColumn: "toStartOfWeek(start_date)", + }, + { + name: "1_year", + + duration: 12 * month, + + wantTable: "agg_events_1d", + wantColumn: "toStartOfMonth(start_date)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var dur *time.Duration + if tt.duration != -1 { + dur = &tt.duration + } + + gotTable, gotColumn := getHistData(dur) + + require.Equal(t, tt.wantTable, gotTable) + require.Equal(t, tt.wantColumn, gotColumn) + }) } } -func TestGetNewErrorGroups(t *testing.T) { +func TestGetErrorGroups(t *testing.T) { var ( service = "test-svc" release = "test-release" @@ -34,186 +114,256 @@ func TestGetNewErrorGroups(t *testing.T) { someErr = errors.New("some err") ) - type mockRows struct { - count int - scanErr error - } - - type mockConn struct { - query string - args []any - - rows *mockRows - err error - } - tests := []struct { name string - req types.GetErrorGroupsRequest - wantGroups int - wantErr bool + req types.GetErrorGroupsRequest + wantGroupsCount int + wantErr bool isSharded bool queryFilter map[string]string - mockConn *mockConn + + mockConns []*mockConnRows }{ { - name: "ok_by_releases", + name: "ok_no_duration", + + req: types.GetErrorGroupsRequest{ + Service: service, + Limit: 10, + Offset: 20, + Order: types.OrderFrequent, + }, + wantGroupsCount: 2, + + mockConns: []*mockConnRows{ + { + query: fmt.Sprintf( + "SELECT _group_hash, source, any(message) as message, countMerge(seen_total) as seen_total, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at"+ + " FROM error_groups"+ + " WHERE service = ? AND _group_hash IN (%s)"+ + " GROUP BY _group_hash, source"+ + " ORDER BY seen_total DESC", + + "SELECT _group_hash"+ + " FROM error_groups"+ + " WHERE service = ?"+ + " GROUP BY _group_hash"+ + " ORDER BY countMerge(seen_total) DESC"+ + " LIMIT 10 OFFSET 20", + ), + args: []any{ + service, + service, + }, + + rows: &mockRowsCount{ + count: 2, + }, + }, + }, + }, + { + name: "ok_duration_frequent", req: types.GetErrorGroupsRequest{ Service: service, - Release: &release, Duration: &duration, - Limit: 20, - Offset: 5, + Limit: 10, + Offset: 20, Order: types.OrderFrequent, }, - wantGroups: 2, - - mockConn: &mockConn{ - query: fmt.Sprintf(""+ - "SELECT _group_hash, source, any(message) as message, countMerge(seen_total) as seen_total, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at"+ - " FROM error_groups"+ - " WHERE _group_hash IN (%s) AND service = 'test-svc'"+ - " GROUP BY _group_hash, source"+ - " ORDER BY seen_total DESC", - - "SELECT _group_hash"+ - " FROM error_groups"+ - " WHERE service = ?"+ - " GROUP BY _group_hash, source"+ - " HAVING any(release) = ? AND count() = ?"+ - " ORDER BY countMerge(seen_total) DESC"+ - " LIMIT 20 OFFSET 5", - ), - args: []any{service, release, 1}, + wantGroupsCount: 2, - rows: &mockRows{ - count: 2, + mockConns: []*mockConnRows{ + { + query: "SELECT _group_hash, countMerge(counts) as count" + + " FROM agg_events_10min" + + " WHERE service = ? AND toStartOfHour(start_date) >= ?" + + " GROUP BY _group_hash" + + " ORDER BY count DESC" + + " LIMIT 10 OFFSET 20", + args: []any{service, timeDiff}, + + rows: &mockRowsScan{ + scanFns: []func(...any) error{ + func(args ...any) error { + *args[0].(*uint64) = 123 + return nil + }, + func(args ...any) error { + *args[0].(*uint64) = 456 + return nil + }, + }, + }, + }, + { + query: "SELECT _group_hash, source, any(message) as message, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at" + + " FROM error_groups" + + " WHERE _group_hash IN (?,?) AND service = ?" + + " GROUP BY _group_hash, source", + args: []any{uint64(123), uint64(456), service}, + + rows: &mockRowsCount{ + count: 2, + }, }, }, }, { - name: "ok_by_duration", + name: "ok_duration_not_frequent", req: types.GetErrorGroupsRequest{ Service: service, Duration: &duration, Limit: 10, + Offset: 20, Order: types.OrderLatest, }, - wantGroups: 2, + wantGroupsCount: 2, - mockConn: &mockConn{ - query: fmt.Sprintf(""+ - "SELECT _group_hash, source, any(message) as message, countMerge(seen_total) as seen_total, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at"+ - " FROM error_groups"+ - " WHERE _group_hash IN (%s) AND service = 'test-svc'"+ - " GROUP BY _group_hash, source"+ - " ORDER BY last_seen_at DESC", + mockConns: []*mockConnRows{ + { + query: fmt.Sprintf( + "SELECT _group_hash, source, any(message) as message, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at"+ + " FROM error_groups"+ + " WHERE service = ? AND _group_hash IN (%s)"+ + " GROUP BY _group_hash, source"+ + " ORDER BY last_seen_at DESC", + + "SELECT _group_hash"+ + " FROM error_groups"+ + " WHERE service = ?"+ + " GROUP BY _group_hash"+ + " HAVING maxMerge(last_seen_at) >= ?"+ + " ORDER BY maxMerge(last_seen_at) DESC"+ + " LIMIT 10 OFFSET 20", + ), + args: []any{ + service, + service, timeDiff, + }, - "SELECT _group_hash"+ - " FROM error_groups"+ - " WHERE service = ?"+ - " GROUP BY _group_hash, source"+ - " HAVING minMerge(first_seen_at) >= ?"+ - " ORDER BY maxMerge(last_seen_at) DESC"+ - " LIMIT 10 OFFSET 0", - ), - args: []any{service, timeDiff}, + rows: &mockRowsScan{ + scanFns: []func(...any) error{ + func(args ...any) error { + *args[0].(*uint64) = 123 + return nil + }, + func(args ...any) error { + *args[0].(*uint64) = 456 + return nil + }, + }, + }, + }, + { + query: "SELECT _group_hash, countMerge(counts) as count" + + " FROM agg_events_10min" + + " WHERE _group_hash IN (?,?) AND service = ?" + + " GROUP BY _group_hash", + args: []any{uint64(123), uint64(456), service}, - rows: &mockRows{ - count: 2, + rows: &mockRowsCount{ + count: 2, + }, }, }, }, { - name: "ok_full_filters", + name: "ok_full_filters_sharded", req: types.GetErrorGroupsRequest{ - Service: service, - Env: &env, - Source: &source, - Duration: &duration, - Limit: 10, - Offset: 20, - Order: types.OrderOldest, + Service: service, + Env: &env, + Source: &source, + Release: &release, + Limit: 10, + Offset: 20, + Order: types.OrderOldest, }, - wantGroups: 2, + wantGroupsCount: 2, + isSharded: true, queryFilter: map[string]string{ "filter1": "value1", "filter2": "value2", }, - mockConn: &mockConn{ - query: fmt.Sprintf(""+ - "SELECT _group_hash, source, any(message) as message, countMerge(seen_total) as seen_total, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at"+ - " FROM error_groups"+ - " WHERE _group_hash IN (%s) AND service = 'test-svc' AND filter1 = 'value1' AND filter2 = 'value2' AND source = 'test-source' AND env = 'test-env'"+ - " GROUP BY _group_hash, source"+ - " ORDER BY first_seen_at", - "SELECT _group_hash"+ - " FROM error_groups"+ - " WHERE service = ? AND filter1 = ? AND filter2 = ? AND env = ? AND source = ?"+ - " GROUP BY _group_hash, source"+ - " HAVING minMerge(first_seen_at) >= ?"+ - " ORDER BY minMerge(first_seen_at)"+ - " LIMIT 10 OFFSET 20", - ), - args: []any{service, "value1", "value2", env, source, timeDiff}, + mockConns: []*mockConnRows{ + { + query: fmt.Sprintf( + "SELECT _group_hash, source, any(message) as message, countMerge(seen_total) as seen_total, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at"+ + " FROM error_groups"+ + " WHERE env = ? AND filter1 = ? AND filter2 = ? AND release = ? AND service = ? AND source = ? AND _group_hash GLOBAL IN (%s)"+ + " GROUP BY _group_hash, source"+ + " ORDER BY first_seen_at", + + "SELECT DISTINCT _group_hash"+ + " FROM error_groups"+ + " WHERE env = ? AND filter1 = ? AND filter2 = ? AND release = ? AND service = ? AND source = ?"+ + " GROUP BY _group_hash"+ + " ORDER BY minMerge(first_seen_at)"+ + " LIMIT 10 OFFSET 20", + ), + args: []any{ + env, "value1", "value2", release, service, source, + env, "value1", "value2", release, service, source, + }, - rows: &mockRows{ - count: 2, + rows: &mockRowsCount{ + count: 2, + }, }, }, }, { - name: "ok_sharded", + name: "ok_no_rows_no_duration", + + req: types.GetErrorGroupsRequest{}, + wantGroupsCount: 0, + + mockConns: []*mockConnRows{ + { + rows: &mockRowsCount{ + count: 0, + }, + }, + }, + }, + { + name: "ok_no_rows_duration_frequent", req: types.GetErrorGroupsRequest{ - Service: service, Duration: &duration, - Limit: 10, - Offset: 20, Order: types.OrderFrequent, }, - wantGroups: 2, - - isSharded: true, - mockConn: &mockConn{ - query: fmt.Sprintf(""+ - "SELECT _group_hash, source, any(message) as message, countMerge(seen_total) as seen_total, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at"+ - " FROM error_groups"+ - " WHERE _group_hash GLOBAL IN (%s) AND service = 'test-svc'"+ - " GROUP BY _group_hash, source"+ - " ORDER BY seen_total DESC", - - "SELECT DISTINCT _group_hash"+ - " FROM error_groups"+ - " WHERE service = ?"+ - " GROUP BY _group_hash, source"+ - " HAVING minMerge(first_seen_at) >= ?"+ - " ORDER BY countMerge(seen_total) DESC"+ - " LIMIT 10 OFFSET 20", - ), - args: []any{service, timeDiff}, + wantGroupsCount: 0, - rows: &mockRows{ - count: 2, + mockConns: []*mockConnRows{ + { + rows: &mockRowsCount{ + count: 0, + }, }, }, }, { - name: "ok_no_rows", + name: "ok_no_rows_duration_no_frequent", - req: types.GetErrorGroupsRequest{}, - wantGroups: 0, + req: types.GetErrorGroupsRequest{ + Duration: &duration, + Order: types.OrderLatest, + }, + wantGroupsCount: 0, - mockConn: &mockConn{ - rows: &mockRows{ - count: 0, + mockConns: []*mockConnRows{ + { + rows: &mockRowsCount{ + count: 0, + }, }, }, }, @@ -223,8 +373,10 @@ func TestGetNewErrorGroups(t *testing.T) { req: types.GetErrorGroupsRequest{}, wantErr: true, - mockConn: &mockConn{ - err: someErr, + mockConns: []*mockConnRows{ + { + err: someErr, + }, }, }, { @@ -233,9 +385,11 @@ func TestGetNewErrorGroups(t *testing.T) { req: types.GetErrorGroupsRequest{}, wantErr: true, - mockConn: &mockConn{ - rows: &mockRows{ - scanErr: someErr, + mockConns: []*mockConnRows{ + { + rows: &mockRowsCount{ + scanErr: someErr, + }, }, }, }, @@ -245,41 +399,18 @@ func TestGetNewErrorGroups(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - ctrl := gomock.NewController(t) - mockedConn := mock.NewMockConn(ctrl) - + mockedConn := initMockConnRows(t, tt.mockConns...) repo := newRepo(mockedConn, tt.isSharded, tt.queryFilter, fakeNow) - if tt.mockConn != nil { - mockedRows := mock.NewMockRows(ctrl) - if rows := tt.mockConn.rows; rows != nil { - times := rows.count - if rows.scanErr != nil { - times = 1 - } - - mockedRows.EXPECT().Next().Return(true).Times(times) - mockedRows.EXPECT().Scan(gomock.Any()).Return(rows.scanErr).Times(times) - if rows.scanErr == nil { - mockedRows.EXPECT().Next().Return(false).Times(1) - } - } - - if tt.mockConn.query == "" { - mockedConn.EXPECT().Query(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockedRows, tt.mockConn.err).Times(1) - } else { - mockedConn.EXPECT().Query(gomock.Any(), tt.mockConn.query, tt.mockConn.args...).Return(mockedRows, tt.mockConn.err).Times(1) - } - } + got, err := repo.GetErrorGroups(context.Background(), tt.req) - got, err := repo.GetNewErrorGroups(context.Background(), tt.req) require.Equal(t, tt.wantErr, err != nil) - require.Equal(t, tt.wantGroups, len(got)) + require.Equal(t, tt.wantGroupsCount, len(got)) }) } } -func TestGetNewErrorGroupsCount(t *testing.T) { +func TestGetErrorGroupsTotal(t *testing.T) { var ( service = "test-svc" release = "test-release" @@ -293,13 +424,6 @@ func TestGetNewErrorGroupsCount(t *testing.T) { someErr = errors.New("some err") ) - type mockConn struct { - query string - args []any - - scanErr error - } - tests := []struct { name string @@ -307,49 +431,26 @@ func TestGetNewErrorGroupsCount(t *testing.T) { wantErr bool queryFilter map[string]string - mockConn *mockConn - }{ - { - name: "ok_by_releases", - - req: types.GetErrorGroupsRequest{ - Service: service, - Release: &release, - Duration: &duration, - }, - mockConn: &mockConn{ - query: fmt.Sprintf(""+ - "SELECT count() FROM (%s) AS subQ", - - "SELECT _group_hash"+ - " FROM error_groups"+ - " WHERE service = ?"+ - " GROUP BY _group_hash, source"+ - " HAVING any(release) = ? AND count() = ?", - ), - args: []any{service, release, 1}, - }, - }, + mockConn *mockConnRow + }{ { - name: "ok_by_duration", + name: "ok", req: types.GetErrorGroupsRequest{ - Service: service, - Duration: &duration, + Service: service, }, - mockConn: &mockConn{ - query: fmt.Sprintf(""+ + mockConn: &mockConnRow{ + query: fmt.Sprintf( "SELECT count() FROM (%s) AS subQ", - "SELECT _group_hash"+ + "SELECT maxMerge(last_seen_at) AS last_seen_at"+ " FROM error_groups"+ " WHERE service = ?"+ - " GROUP BY _group_hash, source"+ - " HAVING minMerge(first_seen_at) >= ?", + " GROUP BY _group_hash", ), - args: []any{service, timeDiff}, + args: []any{service}, }, }, { @@ -357,26 +458,28 @@ func TestGetNewErrorGroupsCount(t *testing.T) { req: types.GetErrorGroupsRequest{ Service: service, - Duration: &duration, Env: &env, Source: &source, + Release: &release, + Duration: &duration, }, queryFilter: map[string]string{ "filter1": "value1", "filter2": "value2", }, - mockConn: &mockConn{ - query: fmt.Sprintf(""+ + + mockConn: &mockConnRow{ + query: fmt.Sprintf( "SELECT count() FROM (%s) AS subQ", - "SELECT _group_hash"+ + "SELECT maxMerge(last_seen_at) AS last_seen_at"+ " FROM error_groups"+ - " WHERE service = ? AND filter1 = ? AND filter2 = ? AND env = ? AND source = ?"+ - " GROUP BY _group_hash, source"+ - " HAVING minMerge(first_seen_at) >= ?", + " WHERE service = ? AND filter1 = ? AND filter2 = ? AND env = ? AND source = ? AND release = ?"+ + " GROUP BY _group_hash"+ + " HAVING last_seen_at >= ?", ), - args: []any{service, "value1", "value2", env, source, timeDiff}, + args: []any{service, "value1", "value2", env, source, release, timeDiff}, }, }, { @@ -384,7 +487,7 @@ func TestGetNewErrorGroupsCount(t *testing.T) { req: types.GetErrorGroupsRequest{}, - mockConn: &mockConn{ + mockConn: &mockConnRow{ scanErr: sql.ErrNoRows, }, }, @@ -394,7 +497,7 @@ func TestGetNewErrorGroupsCount(t *testing.T) { req: types.GetErrorGroupsRequest{}, wantErr: true, - mockConn: &mockConn{ + mockConn: &mockConnRow{ scanErr: someErr, }, }, @@ -404,23 +507,11 @@ func TestGetNewErrorGroupsCount(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - ctrl := gomock.NewController(t) - mockedConn := mock.NewMockConn(ctrl) - + mockedConn := initMockConnRow(t, tt.mockConn) repo := newRepo(mockedConn, true, tt.queryFilter, fakeNow) - if tt.mockConn != nil { - mockedRow := mock.NewMockRow(ctrl) - mockedRow.EXPECT().Scan(gomock.Any()).Return(tt.mockConn.scanErr) - - if tt.mockConn.query == "" { - mockedConn.EXPECT().QueryRow(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockedRow).Times(1) - } else { - mockedConn.EXPECT().QueryRow(gomock.Any(), tt.mockConn.query, tt.mockConn.args...).Return(mockedRow).Times(1) - } - } + got, err := repo.GetErrorGroupsTotal(context.Background(), tt.req) - got, err := repo.GetNewErrorGroupsCount(context.Background(), tt.req) require.Equal(t, tt.wantErr, err != nil) if tt.wantErr { require.Equal(t, uint64(0), got) @@ -429,114 +520,1414 @@ func TestGetNewErrorGroupsCount(t *testing.T) { } } -func TestDiffByReleases(t *testing.T) { +func TestGetNewErrorGroups(t *testing.T) { var ( - service = "test-svc" - releases = []string{"test-release1", "test-release2"} - env = "test-env" - source = "test-source" + service = "test-svc" + release = "test-release" + env = "test-env" + source = "test-source" + + fakeNow = fakeNow(time.Now()) + duration = time.Hour * 24 + timeDiff = fakeNow().Add(-duration.Abs()) someErr = errors.New("some err") ) - type mockRows struct { - scanFns []func(...any) error - scanErr bool - } - - type mockConn struct { - query string - args []any - - rows *mockRows - err error - } - tests := []struct { name string - req types.DiffByReleasesRequest - wantGroups []types.DiffGroup - wantErr bool + req types.GetErrorGroupsRequest + wantGroupsCount int + wantErr bool + + isSharded bool + queryFilter map[string]string - queryFilter map[string]string - mockConnGroups, mockConn *mockConn + mockConn *mockConnRows }{ { - name: "ok", + name: "ok_by_releases", - req: types.DiffByReleasesRequest{ + req: types.GetErrorGroupsRequest{ Service: service, - Releases: releases, - Limit: 20, + Release: &release, + Duration: &duration, + Limit: 20, + Offset: 5, + Order: types.OrderFrequent, + }, + wantGroupsCount: 2, + + mockConn: &mockConnRows{ + query: fmt.Sprintf( + "SELECT _group_hash, source, any(message) as message, countMerge(seen_total) as seen_total, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at"+ + " FROM error_groups"+ + " WHERE service = ? AND _group_hash IN (%s)"+ + " GROUP BY _group_hash, source"+ + " ORDER BY seen_total DESC", + + "SELECT _group_hash"+ + " FROM error_groups"+ + " WHERE service = ?"+ + " GROUP BY _group_hash"+ + " HAVING any(release) = ? AND count() = ?"+ + " ORDER BY countMerge(seen_total) DESC"+ + " LIMIT 20 OFFSET 5", + ), + args: []any{ + service, + service, release, 1, + }, + + rows: &mockRowsCount{ + count: 2, + }, + }, + }, + { + name: "ok_by_duration", + + req: types.GetErrorGroupsRequest{ + Service: service, + Duration: &duration, + Limit: 10, + Order: types.OrderLatest, + }, + wantGroupsCount: 2, + + mockConn: &mockConnRows{ + query: fmt.Sprintf( + "SELECT _group_hash, source, any(message) as message, countMerge(seen_total) as seen_total, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at"+ + " FROM error_groups"+ + " WHERE service = ? AND _group_hash IN (%s)"+ + " GROUP BY _group_hash, source"+ + " ORDER BY last_seen_at DESC", + + "SELECT _group_hash"+ + " FROM error_groups"+ + " WHERE service = ?"+ + " GROUP BY _group_hash"+ + " HAVING minMerge(first_seen_at) >= ?"+ + " ORDER BY maxMerge(last_seen_at) DESC"+ + " LIMIT 10 OFFSET 0", + ), + args: []any{ + service, + service, timeDiff, + }, + + rows: &mockRowsCount{ + count: 2, + }, + }, + }, + { + name: "ok_full_filters", + + req: types.GetErrorGroupsRequest{ + Service: service, + Env: &env, + Source: &source, + Duration: &duration, + Limit: 10, + Offset: 20, + Order: types.OrderOldest, + }, + wantGroupsCount: 2, + + queryFilter: map[string]string{ + "filter1": "value1", + "filter2": "value2", + }, + + mockConn: &mockConnRows{ + query: fmt.Sprintf( + "SELECT _group_hash, source, any(message) as message, countMerge(seen_total) as seen_total, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at"+ + " FROM error_groups"+ + " WHERE env = ? AND filter1 = ? AND filter2 = ? AND service = ? AND source = ? AND _group_hash IN (%s)"+ + " GROUP BY _group_hash, source"+ + " ORDER BY first_seen_at", + + "SELECT _group_hash"+ + " FROM error_groups"+ + " WHERE env = ? AND filter1 = ? AND filter2 = ? AND service = ? AND source = ?"+ + " GROUP BY _group_hash"+ + " HAVING minMerge(first_seen_at) >= ?"+ + " ORDER BY minMerge(first_seen_at)"+ + " LIMIT 10 OFFSET 20", + ), + args: []any{ + env, "value1", "value2", service, source, + env, "value1", "value2", service, source, timeDiff, + }, + + rows: &mockRowsCount{ + count: 2, + }, + }, + }, + { + name: "ok_sharded", + + req: types.GetErrorGroupsRequest{ + Service: service, + Duration: &duration, + Limit: 10, + Offset: 20, + Order: types.OrderFrequent, + }, + wantGroupsCount: 2, + + isSharded: true, + mockConn: &mockConnRows{ + query: fmt.Sprintf( + "SELECT _group_hash, source, any(message) as message, countMerge(seen_total) as seen_total, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at"+ + " FROM error_groups"+ + " WHERE service = ? AND _group_hash GLOBAL IN (%s)"+ + " GROUP BY _group_hash, source"+ + " ORDER BY seen_total DESC", + + "SELECT DISTINCT _group_hash"+ + " FROM error_groups"+ + " WHERE service = ?"+ + " GROUP BY _group_hash"+ + " HAVING minMerge(first_seen_at) >= ?"+ + " ORDER BY countMerge(seen_total) DESC"+ + " LIMIT 10 OFFSET 20", + ), + args: []any{ + service, + service, timeDiff, + }, + + rows: &mockRowsCount{ + count: 2, + }, + }, + }, + { + name: "ok_no_rows", + + req: types.GetErrorGroupsRequest{}, + wantGroupsCount: 0, + + mockConn: &mockConnRows{ + rows: &mockRowsCount{ + count: 0, + }, + }, + }, + { + name: "err_query", + + req: types.GetErrorGroupsRequest{}, + wantErr: true, + + mockConn: &mockConnRows{ + err: someErr, + }, + }, + { + name: "err_scan", + + req: types.GetErrorGroupsRequest{}, + wantErr: true, + + mockConn: &mockConnRows{ + rows: &mockRowsCount{ + scanErr: someErr, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mockedConn := initMockConnRows(t, tt.mockConn) + repo := newRepo(mockedConn, tt.isSharded, tt.queryFilter, fakeNow) + + got, err := repo.GetNewErrorGroups(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + require.Equal(t, tt.wantGroupsCount, len(got)) + }) + } +} + +func TestGetNewErrorGroupsTotal(t *testing.T) { + var ( + service = "test-svc" + release = "test-release" + env = "test-env" + source = "test-source" + + fakeNow = fakeNow(time.Now()) + duration = time.Hour * 24 + timeDiff = fakeNow().Add(-duration.Abs()) + + someErr = errors.New("some err") + ) + + tests := []struct { + name string + + req types.GetErrorGroupsRequest + wantErr bool + + queryFilter map[string]string + + mockConn *mockConnRow + }{ + { + name: "ok_by_releases", + + req: types.GetErrorGroupsRequest{ + Service: service, + Release: &release, + Duration: &duration, + }, + + mockConn: &mockConnRow{ + query: fmt.Sprintf( + "SELECT count() FROM (%s) AS subQ", + + "SELECT _group_hash"+ + " FROM error_groups"+ + " WHERE service = ?"+ + " GROUP BY _group_hash"+ + " HAVING any(release) = ? AND count() = ?", + ), + args: []any{service, release, 1}, + }, + }, + { + name: "ok_by_duration", + + req: types.GetErrorGroupsRequest{ + Service: service, + Duration: &duration, + }, + + mockConn: &mockConnRow{ + query: fmt.Sprintf( + "SELECT count() FROM (%s) AS subQ", + + "SELECT _group_hash"+ + " FROM error_groups"+ + " WHERE service = ?"+ + " GROUP BY _group_hash"+ + " HAVING minMerge(first_seen_at) >= ?", + ), + args: []any{service, timeDiff}, + }, + }, + { + name: "ok_full_filters", + + req: types.GetErrorGroupsRequest{ + Service: service, + Duration: &duration, + Env: &env, + Source: &source, + }, + + queryFilter: map[string]string{ + "filter1": "value1", + "filter2": "value2", + }, + + mockConn: &mockConnRow{ + query: fmt.Sprintf( + "SELECT count() FROM (%s) AS subQ", + + "SELECT _group_hash"+ + " FROM error_groups"+ + " WHERE service = ? AND filter1 = ? AND filter2 = ? AND env = ? AND source = ?"+ + " GROUP BY _group_hash"+ + " HAVING minMerge(first_seen_at) >= ?", + ), + args: []any{service, "value1", "value2", env, source, timeDiff}, + }, + }, + { + name: "ok_no_rows", + + req: types.GetErrorGroupsRequest{}, + + mockConn: &mockConnRow{ + scanErr: sql.ErrNoRows, + }, + }, + { + name: "err_scan", + + req: types.GetErrorGroupsRequest{}, + wantErr: true, + + mockConn: &mockConnRow{ + scanErr: someErr, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mockedConn := initMockConnRow(t, tt.mockConn) + repo := newRepo(mockedConn, true, tt.queryFilter, fakeNow) + + got, err := repo.GetNewErrorGroupsTotal(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { + require.Equal(t, uint64(0), got) + } + }) + } +} + +func TestGetTopErrorGroups(t *testing.T) { + var ( + env = "test-env" + source = "test-source" + + fakeNow = fakeNow(time.Now()) + duration = time.Hour * 24 + timeDiff = fakeNow().Add(-duration.Abs()) + + someErr = errors.New("some err") + ) + + tests := []struct { + name string + + req types.GetTopErrorGroupsRequest + wantGroupsCount int + wantErr bool + + isSharded bool + queryFilter map[string]string + + mockConns []*mockConnRows + }{ + { + name: "ok_no_duration", + + req: types.GetTopErrorGroupsRequest{ + Limit: 10, + Offset: 20, + }, + wantGroupsCount: 2, + + mockConns: []*mockConnRows{ + { + query: fmt.Sprintf( + "SELECT _group_hash, source, any(message) as message, countMerge(seen_total) as seen_total"+ + " FROM error_groups"+ + " WHERE (1=1) AND _group_hash IN (%s)"+ + " GROUP BY _group_hash, source"+ + " ORDER BY seen_total DESC", + + "SELECT _group_hash"+ + " FROM error_groups_brief"+ + " WHERE (1=1)"+ + " GROUP BY _group_hash"+ + " ORDER BY countMerge(seen_total) DESC"+ + " LIMIT 10 OFFSET 20", + ), + args: []any{}, + + rows: &mockRowsCount{ + count: 2, + }, + }, + }, + }, + { + name: "ok_duration", + + req: types.GetTopErrorGroupsRequest{ + Duration: &duration, + Limit: 10, + Offset: 20, + }, + wantGroupsCount: 2, + + mockConns: []*mockConnRows{ + { + query: "SELECT _group_hash, countMerge(counts) as count" + + " FROM agg_events_10min" + + " WHERE (1=1) AND toStartOfHour(start_date) >= ?" + + " GROUP BY _group_hash" + + " ORDER BY count DESC" + + " LIMIT 10 OFFSET 20", + args: []any{timeDiff}, + + rows: &mockRowsScan{ + scanFns: []func(...any) error{ + func(args ...any) error { + *args[0].(*uint64) = 123 + return nil + }, + func(args ...any) error { + *args[0].(*uint64) = 456 + return nil + }, + }, + }, + }, + { + query: "SELECT _group_hash, source, any(message) as message" + + " FROM error_groups" + + " WHERE _group_hash IN (?,?)" + + " GROUP BY _group_hash, source", + args: []any{uint64(123), uint64(456)}, + + rows: &mockRowsCount{ + count: 2, + }, + }, + }, + }, + { + name: "ok_full_filters_sharded", + + req: types.GetTopErrorGroupsRequest{ + Env: &env, + Source: &source, + Limit: 10, + Offset: 20, + }, + wantGroupsCount: 2, + + isSharded: true, + queryFilter: map[string]string{ + "filter1": "value1", + "filter2": "value2", + }, + + mockConns: []*mockConnRows{ + { + query: fmt.Sprintf( + "SELECT _group_hash, source, any(message) as message, countMerge(seen_total) as seen_total"+ + " FROM error_groups"+ + " WHERE env = ? AND filter1 = ? AND filter2 = ? AND source = ? AND _group_hash GLOBAL IN (%s)"+ + " GROUP BY _group_hash, source"+ + " ORDER BY seen_total DESC", + + "SELECT DISTINCT _group_hash"+ + " FROM error_groups_brief"+ + " WHERE env = ? AND filter1 = ? AND filter2 = ? AND source = ?"+ + " GROUP BY _group_hash"+ + " ORDER BY countMerge(seen_total) DESC"+ + " LIMIT 10 OFFSET 20", + ), + args: []any{ + env, "value1", "value2", source, + env, "value1", "value2", source, + }, + + rows: &mockRowsCount{ + count: 2, + }, + }, + }, + }, + { + name: "ok_no_rows_no_duration", + + req: types.GetTopErrorGroupsRequest{}, + wantGroupsCount: 0, + + mockConns: []*mockConnRows{ + { + rows: &mockRowsCount{ + count: 0, + }, + }, + }, + }, + { + name: "ok_no_rows_duration", + + req: types.GetTopErrorGroupsRequest{ + Duration: &duration, + }, + wantGroupsCount: 0, + + mockConns: []*mockConnRows{ + { + rows: &mockRowsCount{ + count: 0, + }, + }, + }, + }, + { + name: "err_query", + + req: types.GetTopErrorGroupsRequest{}, + wantErr: true, + + mockConns: []*mockConnRows{ + { + err: someErr, + }, + }, + }, + { + name: "err_scan", + + req: types.GetTopErrorGroupsRequest{}, + wantErr: true, + + mockConns: []*mockConnRows{ + { + rows: &mockRowsCount{ + scanErr: someErr, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mockedConn := initMockConnRows(t, tt.mockConns...) + repo := newRepo(mockedConn, tt.isSharded, tt.queryFilter, fakeNow) + + got, err := repo.GetTopErrorGroups(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + require.Equal(t, tt.wantGroupsCount, len(got)) + }) + } +} + +func TestGetTopErrorGroupsTotal(t *testing.T) { + var ( + env = "test-env" + source = "test-source" + + fakeNow = fakeNow(time.Now()) + duration = time.Hour * 24 + timeDiff = fakeNow().Add(-duration.Abs()) + + someErr = errors.New("some err") + ) + + tests := []struct { + name string + + req types.GetTopErrorGroupsRequest + wantErr bool + + queryFilter map[string]string + + mockConn *mockConnRow + }{ + { + name: "ok_no_duration", + + req: types.GetTopErrorGroupsRequest{}, + + mockConn: &mockConnRow{ + query: "" + + "SELECT uniq(_group_hash)" + + " FROM error_groups_brief", + + args: []any{}, + }, + }, + { + name: "ok_duration", + + req: types.GetTopErrorGroupsRequest{ + Duration: &duration, + }, + + mockConn: &mockConnRow{ + query: "" + + "SELECT uniq(_group_hash)" + + " FROM agg_events_10min" + + " WHERE toStartOfHour(start_date) >= ?", + + args: []any{timeDiff}, + }, + }, + { + name: "ok_full_filters", + + req: types.GetTopErrorGroupsRequest{ + Env: &env, + Source: &source, + }, + + queryFilter: map[string]string{ + "filter1": "value1", + "filter2": "value2", + }, + + mockConn: &mockConnRow{ + query: "" + + "SELECT uniq(_group_hash)" + + " FROM error_groups_brief" + + " WHERE filter1 = ? AND filter2 = ? AND env = ? AND source = ?", + + args: []any{"value1", "value2", env, source}, + }, + }, + { + name: "ok_no_rows", + + req: types.GetTopErrorGroupsRequest{}, + + mockConn: &mockConnRow{ + scanErr: sql.ErrNoRows, + }, + }, + { + name: "err_scan", + + req: types.GetTopErrorGroupsRequest{}, + wantErr: true, + + mockConn: &mockConnRow{ + scanErr: someErr, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mockedConn := initMockConnRow(t, tt.mockConn) + repo := newRepo(mockedConn, true, tt.queryFilter, fakeNow) + + got, err := repo.GetTopErrorGroupsTotal(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { + require.Equal(t, uint64(0), got) + } + }) + } +} + +func TestDiffByReleases(t *testing.T) { + var ( + service = "test-svc" + releases = []string{"test-release1", "test-release2"} + env = "test-env" + source = "test-source" + + someErr = errors.New("some err") + ) + + tests := []struct { + name string + + req types.DiffByReleasesRequest + wantGroups []types.DiffGroup + wantErr bool + + queryFilter map[string]string + + mockConns []*mockConnRows + }{ + { + name: "ok", + + req: types.DiffByReleasesRequest{ + Service: service, + Releases: releases, + Limit: 20, Order: types.OrderFrequent, }, wantGroups: []types.DiffGroup{ { - Hash: 123, - ReleaseInfos: map[string]types.DiffReleaseInfo{ - releases[0]: {SeenTotal: 10}, - releases[1]: {SeenTotal: 20}, + Hash: 123, + ReleaseInfos: map[string]types.DiffReleaseInfo{ + releases[0]: {SeenTotal: 10}, + releases[1]: {SeenTotal: 20}, + }, + }, + { + Hash: 456, + ReleaseInfos: map[string]types.DiffReleaseInfo{ + releases[0]: {SeenTotal: 0}, + releases[1]: {SeenTotal: 1000}, + }, + }, + }, + + mockConns: []*mockConnRows{ + { + query: "" + + "SELECT _group_hash, source, any(message) as message, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at" + + " FROM error_groups" + + " WHERE release IN (?,?) AND service = ?" + + " GROUP BY _group_hash, source" + + " ORDER BY countMerge(seen_total) DESC" + + " LIMIT 20 OFFSET 0", + args: []any{releases[0], releases[1], service}, + + rows: &mockRowsScan{ + scanFns: []func(...any) error{ + func(args ...any) error { + *args[0].(*uint64) = 123 + return nil + }, + func(args ...any) error { + *args[0].(*uint64) = 456 + return nil + }, + }, + }, + }, + { + query: "" + + "SELECT _group_hash, release, countMerge(seen_total) as seen_total" + + " FROM error_groups" + + " WHERE _group_hash IN (?,?) AND release IN (?,?) AND service = ?" + + " GROUP BY _group_hash, release", + args: []any{uint64(123), uint64(456), releases[0], releases[1], service}, + + rows: &mockRowsScan{ + scanFns: []func(...any) error{ + func(args ...any) error { + *args[0].(*uint64) = 123 + *args[1].(*string) = releases[0] + *args[2].(*uint64) = 10 + return nil + }, + func(args ...any) error { + *args[0].(*uint64) = 123 + *args[1].(*string) = releases[1] + *args[2].(*uint64) = 20 + return nil + }, + func(args ...any) error { + *args[0].(*uint64) = 456 + *args[1].(*string) = releases[1] + *args[2].(*uint64) = 1000 + return nil + }, + }, + }, + }, + }, + }, + { + name: "ok_full_filters", + + req: types.DiffByReleasesRequest{ + Service: service, + Releases: releases, + Env: &env, + Source: &source, + Limit: 20, + Offset: 5, + Order: types.OrderLatest, + }, + wantGroups: []types.DiffGroup{ + { + Hash: 123, + ReleaseInfos: map[string]types.DiffReleaseInfo{ + releases[0]: {SeenTotal: 10}, + releases[1]: {SeenTotal: 20}, + }, + }, + { + Hash: 456, + ReleaseInfos: map[string]types.DiffReleaseInfo{ + releases[0]: {SeenTotal: 0}, + releases[1]: {SeenTotal: 1000}, + }, + }, + }, + + queryFilter: map[string]string{ + "filter1": "value1", + "filter2": "value2", + }, + mockConns: []*mockConnRows{ + { + query: "" + + "SELECT _group_hash, source, any(message) as message, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at" + + " FROM error_groups" + + " WHERE env = ? AND filter1 = ? AND filter2 = ? AND release IN (?,?) AND service = ? AND source = ?" + + " GROUP BY _group_hash, source" + + " ORDER BY last_seen_at DESC" + + " LIMIT 20 OFFSET 5", + args: []any{env, "value1", "value2", releases[0], releases[1], service, source}, + + rows: &mockRowsScan{ + scanFns: []func(...any) error{ + func(args ...any) error { + *args[0].(*uint64) = 123 + return nil + }, + func(args ...any) error { + *args[0].(*uint64) = 456 + return nil + }, + }, + }, + }, + { + query: "" + + "SELECT _group_hash, release, countMerge(seen_total) as seen_total" + + " FROM error_groups" + + " WHERE _group_hash IN (?,?) AND env = ? AND filter1 = ? AND filter2 = ? AND release IN (?,?) AND service = ? AND source = ?" + + " GROUP BY _group_hash, release", + args: []any{uint64(123), uint64(456), env, "value1", "value2", releases[0], releases[1], service, source}, + + rows: &mockRowsScan{ + scanFns: []func(...any) error{ + func(args ...any) error { + *args[0].(*uint64) = 123 + *args[1].(*string) = releases[0] + *args[2].(*uint64) = 10 + return nil + }, + func(args ...any) error { + *args[0].(*uint64) = 123 + *args[1].(*string) = releases[1] + *args[2].(*uint64) = 20 + return nil + }, + func(args ...any) error { + *args[0].(*uint64) = 456 + *args[1].(*string) = releases[1] + *args[2].(*uint64) = 1000 + return nil + }, + }, + }, + }, + }, + }, + { + name: "ok_no_rows", + + req: types.DiffByReleasesRequest{}, + wantGroups: nil, + + mockConns: []*mockConnRows{ + { + rows: &mockRowsCount{ + count: 0, + }, + }, + }, + }, + { + name: "err_query_groups", + + req: types.DiffByReleasesRequest{}, + wantErr: true, + + mockConns: []*mockConnRows{ + {err: someErr}, + }, + }, + { + name: "err_scan_groups", + + req: types.DiffByReleasesRequest{}, + wantErr: true, + + mockConns: []*mockConnRows{ + { + rows: &mockRowsScan{ + scanFns: []func(...any) error{ + func(args ...any) error { return someErr }, + }, + scanErr: true, + }, + }, + }, + }, + { + name: "err_query", + + req: types.DiffByReleasesRequest{}, + wantErr: true, + + mockConns: []*mockConnRows{ + { + rows: &mockRowsScan{ + scanFns: []func(...any) error{ + func(args ...any) error { + *args[0].(*uint64) = 123 + return nil + }, + }, + }, + }, + { + err: someErr, + }, + }, + }, + { + name: "err_scan", + + req: types.DiffByReleasesRequest{}, + wantErr: true, + + mockConns: []*mockConnRows{ + { + rows: &mockRowsScan{ + scanFns: []func(...any) error{ + func(args ...any) error { + *args[0].(*uint64) = 123 + return nil + }, + }, }, }, { - Hash: 456, - ReleaseInfos: map[string]types.DiffReleaseInfo{ - releases[0]: {SeenTotal: 0}, - releases[1]: {SeenTotal: 1000}, + rows: &mockRowsScan{ + scanFns: []func(...any) error{ + func(args ...any) error { return someErr }, + }, + scanErr: true, }, }, }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mockedConn := initMockConnRows(t, tt.mockConns...) + repo := newRepo(mockedConn, true, tt.queryFilter, time.Now) + + got, err := repo.DiffByReleases(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { + return + } + + require.Equal(t, tt.wantGroups, got) + }) + } +} + +func TestDiffByReleasesTotal(t *testing.T) { + var ( + service = "test-svc" + releases = []string{"test-release1", "test-release2"} + env = "test-env" + source = "test-source" + + someErr = errors.New("some err") + ) + + tests := []struct { + name string + + req types.DiffByReleasesRequest + wantErr bool + + queryFilter map[string]string + + mockConn *mockConnRow + }{ + { + name: "ok", + + req: types.DiffByReleasesRequest{ + Service: service, + Releases: releases, + }, + + mockConn: &mockConnRow{ + query: fmt.Sprintf( + "SELECT count() FROM (%s) AS subQ", + + "SELECT _group_hash"+ + " FROM error_groups"+ + " WHERE release IN (?,?) AND service = ?"+ + " GROUP BY _group_hash", + ), + args: []any{releases[0], releases[1], service}, + }, + }, + { + name: "ok_full_filters", + + req: types.DiffByReleasesRequest{ + Service: service, + Releases: releases, + Env: &env, + Source: &source, + }, + + queryFilter: map[string]string{ + "filter1": "value1", + "filter2": "value2", + }, + + mockConn: &mockConnRow{ + query: fmt.Sprintf( + "SELECT count() FROM (%s) AS subQ", + + "SELECT _group_hash"+ + " FROM error_groups"+ + " WHERE release IN (?,?) AND service = ? AND filter1 = ? AND filter2 = ? AND env = ? AND source = ?"+ + " GROUP BY _group_hash", + ), + args: []any{releases[0], releases[1], service, "value1", "value2", env, source}, + }, + }, + { + name: "ok_no_rows", + + req: types.DiffByReleasesRequest{}, + + mockConn: &mockConnRow{ + scanErr: sql.ErrNoRows, + }, + }, + { + name: "err_scan", + + req: types.DiffByReleasesRequest{}, + wantErr: true, + + mockConn: &mockConnRow{ + scanErr: someErr, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mockedConn := initMockConnRow(t, tt.mockConn) + repo := newRepo(mockedConn, true, tt.queryFilter, time.Now) + + got, err := repo.DiffByReleasesTotal(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { + require.Equal(t, uint64(0), got) + } + }) + } +} + +func TestGetErrorHist(t *testing.T) { + var ( + groupHash = uint64(123) + service = "test-svc" + release = "test-release" + env = "test-env" + source = "test-source" + + fakeNow = fakeNow(time.Now()) + duration = time.Hour + timeDiff = fakeNow().Add(-duration.Abs()) + + someErr = errors.New("some err") + ) + + tests := []struct { + name string + + req types.GetErrorHistRequest + wantBucketsCount int + wantErr bool + + queryFilter map[string]string + + mockConn *mockConnRows + }{ + { + name: "ok", + + req: types.GetErrorHistRequest{}, + wantBucketsCount: 2, + + mockConn: &mockConnRows{ + query: "" + + "SELECT toStartOfMonth(start_date), countMerge(counts) as counts" + + " FROM agg_events_1d" + + " GROUP BY toStartOfMonth(start_date)" + + " ORDER BY toStartOfMonth(start_date)", + args: []any{}, + + rows: &mockRowsCount{ + count: 2, + }, + }, + }, + { + name: "ok_full_filters", + + req: types.GetErrorHistRequest{ + GroupHash: &groupHash, + Service: &service, + Env: &env, + Source: &source, + Release: &release, + Duration: &duration, + }, + wantBucketsCount: 2, + + queryFilter: map[string]string{ + "filter1": "value1", + "filter2": "value2", + }, + + mockConn: &mockConnRows{ + query: "" + + "SELECT start_date, countMerge(counts) as counts" + + " FROM agg_events_10min" + + " WHERE filter1 = ? AND filter2 = ? AND _group_hash = ? AND env = ? AND source = ? AND service = ? AND release = ? AND start_date >= ?" + + " GROUP BY start_date" + + " ORDER BY start_date", + args: []any{"value1", "value2", groupHash, env, source, service, release, timeDiff}, + + rows: &mockRowsCount{ + count: 2, + }, + }, + }, + { + name: "ok_no_rows", + + req: types.GetErrorHistRequest{}, + wantBucketsCount: 0, + + mockConn: &mockConnRows{ + rows: &mockRowsCount{ + count: 0, + }, + }, + }, + { + name: "err_query", + + req: types.GetErrorHistRequest{}, + wantErr: true, + + mockConn: &mockConnRows{ + err: someErr, + }, + }, + { + name: "err_scan", + + req: types.GetErrorHistRequest{}, + wantErr: true, + + mockConn: &mockConnRows{ + rows: &mockRowsCount{ + scanErr: someErr, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mockedConn := initMockConnRows(t, tt.mockConn) + repo := newRepo(mockedConn, true, tt.queryFilter, fakeNow) + + got, err := repo.GetErrorHist(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + require.Equal(t, tt.wantBucketsCount, len(got)) + }) + } +} + +func TestGetErrorDetails(t *testing.T) { + var ( + groupHash = uint64(123) + service = "test-svc" + release = "test-release" + env = "test-env" + source = "test-source" + + fakeNow = fakeNow(time.Now()) + + someErr = errors.New("some err") + ) + + tests := []struct { + name string + + req types.GetErrorGroupDetailsRequest + wantErr bool + + queryFilter map[string]string + + mockConn *mockConnRow + }{ + { + name: "ok", + + req: types.GetErrorGroupDetailsRequest{ + GroupHash: groupHash, + }, + + mockConn: &mockConnRow{ + query: "" + + "SELECT _group_hash, source, any(message) as message, countMerge(seen_total) as seen_total, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at, max(log_tags) as log_tags" + + " FROM error_groups" + + " WHERE _group_hash = ?" + + " GROUP BY _group_hash, source", + args: []any{groupHash}, + }, + }, + { + name: "ok_full_filters", + + req: types.GetErrorGroupDetailsRequest{ + GroupHash: groupHash, + Env: &env, + Source: &source, + Service: &service, + Release: &release, + }, + + queryFilter: map[string]string{ + "filter1": "value1", + "filter2": "value2", + }, + + mockConn: &mockConnRow{ + query: "" + + "SELECT _group_hash, source, any(message) as message, countMerge(seen_total) as seen_total, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at, max(log_tags) as log_tags" + + " FROM error_groups" + + " WHERE _group_hash = ? AND filter1 = ? AND filter2 = ? AND env = ? AND source = ? AND service = ? AND release = ?" + + " GROUP BY _group_hash, source", + args: []any{groupHash, "value1", "value2", env, source, service, release}, + }, + }, + { + name: "ok_no_rows", + + req: types.GetErrorGroupDetailsRequest{}, + + mockConn: &mockConnRow{ + scanErr: sql.ErrNoRows, + }, + }, + { + name: "err_scan", + + req: types.GetErrorGroupDetailsRequest{}, + wantErr: true, + + mockConn: &mockConnRow{ + scanErr: someErr, + }, + }, + } - mockConnGroups: &mockConn{ - query: "" + - "SELECT _group_hash, source, any(message) as message, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at" + - " FROM error_groups" + - " WHERE release IN (?,?) AND service = ?" + - " GROUP BY _group_hash, source" + - " ORDER BY countMerge(seen_total) DESC" + - " LIMIT 20 OFFSET 0", - args: []any{releases[0], releases[1], service}, + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() - rows: &mockRows{ - scanFns: []func(...any) error{ - func(args ...any) error { - *args[0].(*uint64) = 123 - return nil - }, - func(args ...any) error { - *args[0].(*uint64) = 456 - return nil - }, - }, + mockedConn := initMockConnRow(t, tt.mockConn) + repo := newRepo(mockedConn, true, tt.queryFilter, fakeNow) + + _, err := repo.GetErrorDetails(context.Background(), tt.req) + require.Equal(t, tt.wantErr, err != nil) + }) + } +} + +func TestGetErrorCounts(t *testing.T) { + var ( + groupHash = uint64(123) + service = "test-svc" + release = "test-release" + env = "test-env" + source = "test-source" + + fakeNow = fakeNow(time.Now()) + + someErr = errors.New("some err") + ) + + tests := []struct { + name string + + req types.GetErrorGroupDetailsRequest + want types.ErrorGroupCounts + wantErr bool + + queryFilter map[string]string + + mockConn *mockConnRows + }{ + { + name: "ok", + + req: types.GetErrorGroupDetailsRequest{ + GroupHash: groupHash, + }, + want: types.ErrorGroupCounts{ + ByEnv: types.ErrorGroupCount{ + "env1": 10, + "env2": 20, + }, + BySource: types.ErrorGroupCount{ + "source1": 10, + "source2": 20, }, + ByService: types.ErrorGroupCount{ + "service1": 30, + }, + ByRelease: types.ErrorGroupCount{}, }, - mockConn: &mockConn{ + + mockConn: &mockConnRows{ query: "" + - "SELECT _group_hash, release, countMerge(seen_total) as seen_total" + + "SELECT countMerge(seen_total) as count, env, source, service" + " FROM error_groups" + - " WHERE _group_hash IN (?,?) AND release IN (?,?) AND service = ?" + - " GROUP BY _group_hash, release", - args: []any{uint64(123), uint64(456), releases[0], releases[1], service}, - - rows: &mockRows{ - scanFns: []func(...any) error{ - func(args ...any) error { - *args[0].(*uint64) = 123 - *args[1].(*string) = releases[0] - *args[2].(*uint64) = 10 - return nil - }, - func(args ...any) error { - *args[0].(*uint64) = 123 - *args[1].(*string) = releases[1] - *args[2].(*uint64) = 20 + " WHERE _group_hash = ?" + + " GROUP BY env, source, service", + args: []any{groupHash}, + + rows: &mockRowsScanStruct{ + scanStructFns: []func(any) error{ + func(ec any) error { + *ec.(*errCounts) = errCounts{ + count: 10, + env: "env1", + source: "source1", + service: "service1", + } return nil }, - func(args ...any) error { - *args[0].(*uint64) = 456 - *args[1].(*string) = releases[1] - *args[2].(*uint64) = 1000 + func(ec any) error { + *ec.(*errCounts) = errCounts{ + count: 20, + env: "env2", + source: "source2", + service: "service1", + } return nil }, }, @@ -546,85 +1937,43 @@ func TestDiffByReleases(t *testing.T) { { name: "ok_full_filters", - req: types.DiffByReleasesRequest{ - Service: service, - Releases: releases, - Env: &env, - Source: &source, - Limit: 20, - Offset: 5, - Order: types.OrderLatest, + req: types.GetErrorGroupDetailsRequest{ + GroupHash: groupHash, + Env: &env, + Source: &source, + Service: &service, + Release: &release, }, - wantGroups: []types.DiffGroup{ - { - Hash: 123, - ReleaseInfos: map[string]types.DiffReleaseInfo{ - releases[0]: {SeenTotal: 10}, - releases[1]: {SeenTotal: 20}, - }, - }, - { - Hash: 456, - ReleaseInfos: map[string]types.DiffReleaseInfo{ - releases[0]: {SeenTotal: 0}, - releases[1]: {SeenTotal: 1000}, - }, - }, + want: types.ErrorGroupCounts{ + ByEnv: types.ErrorGroupCount{env: 10}, + BySource: types.ErrorGroupCount{source: 10}, + ByService: types.ErrorGroupCount{service: 10}, + ByRelease: types.ErrorGroupCount{release: 10}, }, queryFilter: map[string]string{ "filter1": "value1", "filter2": "value2", }, - mockConnGroups: &mockConn{ - query: "" + - "SELECT _group_hash, source, any(message) as message, minMerge(first_seen_at) as first_seen_at, maxMerge(last_seen_at) as last_seen_at" + - " FROM error_groups" + - " WHERE env = ? AND filter1 = ? AND filter2 = ? AND release IN (?,?) AND service = ? AND source = ?" + - " GROUP BY _group_hash, source" + - " ORDER BY last_seen_at DESC" + - " LIMIT 20 OFFSET 5", - args: []any{env, "value1", "value2", releases[0], releases[1], service, source}, - - rows: &mockRows{ - scanFns: []func(...any) error{ - func(args ...any) error { - *args[0].(*uint64) = 123 - return nil - }, - func(args ...any) error { - *args[0].(*uint64) = 456 - return nil - }, - }, - }, - }, - mockConn: &mockConn{ + + mockConn: &mockConnRows{ query: "" + - "SELECT _group_hash, release, countMerge(seen_total) as seen_total" + + "SELECT countMerge(seen_total) as count, env, source, service, release" + " FROM error_groups" + - " WHERE _group_hash IN (?,?) AND env = ? AND filter1 = ? AND filter2 = ? AND release IN (?,?) AND service = ? AND source = ?" + - " GROUP BY _group_hash, release", - args: []any{uint64(123), uint64(456), env, "value1", "value2", releases[0], releases[1], service, source}, - - rows: &mockRows{ - scanFns: []func(...any) error{ - func(args ...any) error { - *args[0].(*uint64) = 123 - *args[1].(*string) = releases[0] - *args[2].(*uint64) = 10 - return nil - }, - func(args ...any) error { - *args[0].(*uint64) = 123 - *args[1].(*string) = releases[1] - *args[2].(*uint64) = 20 - return nil - }, - func(args ...any) error { - *args[0].(*uint64) = 456 - *args[1].(*string) = releases[1] - *args[2].(*uint64) = 1000 + " WHERE _group_hash = ? AND filter1 = ? AND filter2 = ? AND env = ? AND source = ? AND service = ? AND release = ?" + + " GROUP BY env, source, service, release", + args: []any{groupHash, "value1", "value2", env, source, service, release}, + + rows: &mockRowsScanStruct{ + scanStructFns: []func(any) error{ + func(ec any) error { + *ec.(*errCounts) = errCounts{ + count: 10, + env: env, + source: source, + service: service, + release: release, + } return nil }, }, @@ -634,80 +1983,169 @@ func TestDiffByReleases(t *testing.T) { { name: "ok_no_rows", - req: types.DiffByReleasesRequest{}, - wantGroups: nil, + req: types.GetErrorGroupDetailsRequest{}, + want: types.ErrorGroupCounts{ + ByEnv: types.ErrorGroupCount{}, + BySource: types.ErrorGroupCount{}, + ByService: types.ErrorGroupCount{}, + ByRelease: types.ErrorGroupCount{}, + }, - mockConnGroups: &mockConn{ - rows: &mockRows{}, + mockConn: &mockConnRows{ + rows: &mockRowsCount{ + count: 0, + }, }, }, { - name: "err_query_groups", + name: "err_query", - req: types.DiffByReleasesRequest{}, + req: types.GetErrorGroupDetailsRequest{}, wantErr: true, - mockConnGroups: &mockConn{ + mockConn: &mockConnRows{ err: someErr, }, }, { - name: "err_scan_groups", + name: "err_scan", - req: types.DiffByReleasesRequest{}, + req: types.GetErrorGroupDetailsRequest{}, wantErr: true, - mockConnGroups: &mockConn{ - rows: &mockRows{ - scanErr: true, - scanFns: []func(...any) error{ - func(args ...any) error { return someErr }, + mockConn: &mockConnRows{ + rows: &mockRowsScanStruct{ + scanStructFns: []func(any) error{ + func(_ any) error { + return someErr + }, }, + scanErr: true, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mockedConn := initMockConnRows(t, tt.mockConn) + repo := newRepo(mockedConn, true, tt.queryFilter, fakeNow) + + got, err := repo.GetErrorCounts(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { + return + } + + require.Equal(t, tt.want, got) + }) + } +} + +func TestGetServices(t *testing.T) { + var ( + query = "test" + env = "test-env" + + fakeNow = fakeNow(time.Now()) + + someErr = errors.New("some err") + ) + + tests := []struct { + name string + + req types.GetServicesRequest + wantServicesCount int + wantErr bool + + queryFilter map[string]string + + mockConn *mockConnRows + }{ + { + name: "ok", + + req: types.GetServicesRequest{}, + wantServicesCount: 2, + + mockConn: &mockConnRows{ + query: "" + + "SELECT DISTINCT service" + + " FROM services" + + " WHERE service <> ?" + + " ORDER BY service", + args: []any{""}, + + rows: &mockRowsCount{ + count: 2, }, }, }, { - name: "err_query", + name: "ok_full_filters", - req: types.DiffByReleasesRequest{}, - wantErr: true, + req: types.GetServicesRequest{ + Query: query, + Env: &env, + Limit: 5, + Offset: 10, + }, + wantServicesCount: 5, - mockConnGroups: &mockConn{ - rows: &mockRows{ - scanFns: []func(...any) error{ - func(args ...any) error { - *args[0].(*uint64) = 123 - return nil - }, - }, + queryFilter: map[string]string{ + "filter1": "value1", + "filter2": "value2", + }, + + mockConn: &mockConnRows{ + query: "" + + "SELECT DISTINCT service" + + " FROM services" + + " WHERE service <> ? AND filter1 = ? AND filter2 = ? AND startsWith(service, ?) AND env = ?" + + " ORDER BY service" + + " LIMIT 5 OFFSET 10", + args: []any{"", "value1", "value2", query, env}, + + rows: &mockRowsCount{ + count: 5, + }, + }, + }, + { + name: "ok_no_rows", + + req: types.GetServicesRequest{}, + wantServicesCount: 0, + + mockConn: &mockConnRows{ + rows: &mockRowsCount{ + count: 0, }, }, - mockConn: &mockConn{ + }, + { + name: "err_query", + + req: types.GetServicesRequest{}, + wantErr: true, + + mockConn: &mockConnRows{ err: someErr, }, }, { name: "err_scan", - req: types.DiffByReleasesRequest{}, + req: types.GetServicesRequest{}, wantErr: true, - mockConnGroups: &mockConn{ - rows: &mockRows{ - scanFns: []func(...any) error{ - func(args ...any) error { - *args[0].(*uint64) = 123 - return nil - }, - }, - }, - }, - mockConn: &mockConn{ - rows: &mockRows{ - scanErr: true, - scanFns: []func(...any) error{ - func(args ...any) error { return someErr }, - }, + mockConn: &mockConnRows{ + rows: &mockRowsCount{ + scanErr: someErr, }, }, }, @@ -717,137 +2155,118 @@ func TestDiffByReleases(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - ctrl := gomock.NewController(t) - mockedConn := mock.NewMockConn(ctrl) - - repo := newRepo(mockedConn, true, tt.queryFilter, time.Now) - - initMockConn := func(mc *mockConn) { - if mc == nil { - return - } - - mockedRows := mock.NewMockRows(ctrl) - if rows := mc.rows; rows != nil { - for _, scanFn := range rows.scanFns { - mockedRows.EXPECT().Next().Return(true) - mockedRows.EXPECT().Scan(gomock.Any()).DoAndReturn(scanFn) - } - if !rows.scanErr { - mockedRows.EXPECT().Next().Return(false) - } - } - - if mc.query == "" { - mockedConn.EXPECT().Query(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockedRows, mc.err).Times(1) - } else { - mockedConn.EXPECT().Query(gomock.Any(), mc.query, mc.args...).Return(mockedRows, mc.err).Times(1) - } - } + mockedConn := initMockConnRows(t, tt.mockConn) + repo := newRepo(mockedConn, true, tt.queryFilter, fakeNow) - initMockConn(tt.mockConnGroups) - initMockConn(tt.mockConn) + got, err := repo.GetServices(context.Background(), tt.req) - got, err := repo.DiffByReleases(context.Background(), tt.req) require.Equal(t, tt.wantErr, err != nil) - if tt.wantErr { - return - } - - require.Equal(t, tt.wantGroups, got) + require.Equal(t, tt.wantServicesCount, len(got)) }) } } -func TestDiffByReleasesTotal(t *testing.T) { +func TestGetReleases(t *testing.T) { var ( - service = "test-svc" - releases = []string{"test-release1", "test-release2"} - env = "test-env" - source = "test-source" + service = "test-service" + env = "test-env" + + fakeNow = fakeNow(time.Now()) someErr = errors.New("some err") ) - type mockConn struct { - query string - args []any - - scanErr error - } - tests := []struct { name string - req types.DiffByReleasesRequest - wantErr bool + req types.GetReleasesRequest + wantReleasesCount int + wantErr bool queryFilter map[string]string - mockConn *mockConn + + mockConn *mockConnRows }{ { name: "ok", - req: types.DiffByReleasesRequest{ - Service: service, - Releases: releases, + req: types.GetReleasesRequest{ + Service: service, }, + wantReleasesCount: 2, - mockConn: &mockConn{ - query: fmt.Sprintf(""+ - "SELECT count() FROM (%s) AS subQ", + mockConn: &mockConnRows{ + query: "" + + "SELECT DISTINCT release" + + " FROM services" + + " WHERE (service = ? AND release <> ?)" + + " ORDER BY ttl DESC", + args: []any{service, ""}, - "SELECT _group_hash"+ - " FROM error_groups"+ - " WHERE release IN (?,?) AND service = ?"+ - " GROUP BY _group_hash", - ), - args: []any{releases[0], releases[1], service}, + rows: &mockRowsCount{ + count: 2, + }, }, }, { name: "ok_full_filters", - req: types.DiffByReleasesRequest{ - Service: service, - Releases: releases, - Env: &env, - Source: &source, + req: types.GetReleasesRequest{ + Service: service, + Env: &env, }, + wantReleasesCount: 2, queryFilter: map[string]string{ "filter1": "value1", "filter2": "value2", }, - mockConn: &mockConn{ - query: fmt.Sprintf(""+ - "SELECT count() FROM (%s) AS subQ", - "SELECT _group_hash"+ - " FROM error_groups"+ - " WHERE release IN (?,?) AND service = ? AND filter1 = ? AND filter2 = ? AND env = ? AND source = ?"+ - " GROUP BY _group_hash", - ), - args: []any{releases[0], releases[1], service, "value1", "value2", env, source}, + mockConn: &mockConnRows{ + query: "" + + "SELECT DISTINCT release" + + " FROM services" + + " WHERE (service = ? AND release <> ?) AND filter1 = ? AND filter2 = ? AND env = ?" + + " ORDER BY ttl DESC", + args: []any{service, "", "value1", "value2", env}, + + rows: &mockRowsCount{ + count: 2, + }, }, }, { name: "ok_no_rows", - req: types.DiffByReleasesRequest{}, + req: types.GetReleasesRequest{}, + wantReleasesCount: 0, - mockConn: &mockConn{ - scanErr: sql.ErrNoRows, + mockConn: &mockConnRows{ + rows: &mockRowsCount{ + count: 0, + }, + }, + }, + { + name: "err_query", + + req: types.GetReleasesRequest{}, + wantErr: true, + + mockConn: &mockConnRows{ + err: someErr, }, }, { name: "err_scan", - req: types.DiffByReleasesRequest{}, + req: types.GetReleasesRequest{}, wantErr: true, - mockConn: &mockConn{ - scanErr: someErr, + mockConn: &mockConnRows{ + rows: &mockRowsCount{ + scanErr: someErr, + }, }, }, } @@ -856,27 +2275,12 @@ func TestDiffByReleasesTotal(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - ctrl := gomock.NewController(t) - mockedConn := mock.NewMockConn(ctrl) - - repo := newRepo(mockedConn, true, tt.queryFilter, time.Now) - - if tt.mockConn != nil { - mockedRow := mock.NewMockRow(ctrl) - mockedRow.EXPECT().Scan(gomock.Any()).Return(tt.mockConn.scanErr) - - if tt.mockConn.query == "" { - mockedConn.EXPECT().QueryRow(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockedRow).Times(1) - } else { - mockedConn.EXPECT().QueryRow(gomock.Any(), tt.mockConn.query, tt.mockConn.args...).Return(mockedRow).Times(1) - } - } + mockedConn := initMockConnRows(t, tt.mockConn) + repo := newRepo(mockedConn, true, tt.queryFilter, fakeNow) - got, err := repo.DiffByReleasesTotal(context.Background(), tt.req) + got, err := repo.GetReleases(context.Background(), tt.req) require.Equal(t, tt.wantErr, err != nil) - if tt.wantErr { - require.Equal(t, uint64(0), got) - } + require.Equal(t, tt.wantReleasesCount, len(got)) }) } } diff --git a/internal/pkg/repository_ch/mock/repository_ch.go b/internal/pkg/repository_ch/mock/repository_ch.go index d89a611..fd287cc 100644 --- a/internal/pkg/repository_ch/mock/repository_ch.go +++ b/internal/pkg/repository_ch/mock/repository_ch.go @@ -116,19 +116,19 @@ func (mr *MockRepositoryMockRecorder) GetErrorGroups(arg0, arg1 any) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetErrorGroups", reflect.TypeOf((*MockRepository)(nil).GetErrorGroups), arg0, arg1) } -// GetErrorGroupsCount mocks base method. -func (m *MockRepository) GetErrorGroupsCount(arg0 context.Context, arg1 types.GetErrorGroupsRequest) (uint64, error) { +// GetErrorGroupsTotal mocks base method. +func (m *MockRepository) GetErrorGroupsTotal(arg0 context.Context, arg1 types.GetErrorGroupsRequest) (uint64, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetErrorGroupsCount", arg0, arg1) + ret := m.ctrl.Call(m, "GetErrorGroupsTotal", arg0, arg1) ret0, _ := ret[0].(uint64) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetErrorGroupsCount indicates an expected call of GetErrorGroupsCount. -func (mr *MockRepositoryMockRecorder) GetErrorGroupsCount(arg0, arg1 any) *gomock.Call { +// GetErrorGroupsTotal indicates an expected call of GetErrorGroupsTotal. +func (mr *MockRepositoryMockRecorder) GetErrorGroupsTotal(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetErrorGroupsCount", reflect.TypeOf((*MockRepository)(nil).GetErrorGroupsCount), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetErrorGroupsTotal", reflect.TypeOf((*MockRepository)(nil).GetErrorGroupsTotal), arg0, arg1) } // GetErrorHist mocks base method. @@ -146,21 +146,6 @@ func (mr *MockRepositoryMockRecorder) GetErrorHist(arg0, arg1 any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetErrorHist", reflect.TypeOf((*MockRepository)(nil).GetErrorHist), arg0, arg1) } -// GetErrorReleases mocks base method. -func (m *MockRepository) GetErrorReleases(arg0 context.Context, arg1 types.GetErrorGroupReleasesRequest) ([]string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetErrorReleases", arg0, arg1) - ret0, _ := ret[0].([]string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetErrorReleases indicates an expected call of GetErrorReleases. -func (mr *MockRepositoryMockRecorder) GetErrorReleases(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetErrorReleases", reflect.TypeOf((*MockRepository)(nil).GetErrorReleases), arg0, arg1) -} - // GetNewErrorGroups mocks base method. func (m *MockRepository) GetNewErrorGroups(arg0 context.Context, arg1 types.GetErrorGroupsRequest) ([]types.ErrorGroup, error) { m.ctrl.T.Helper() @@ -176,19 +161,34 @@ func (mr *MockRepositoryMockRecorder) GetNewErrorGroups(arg0, arg1 any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNewErrorGroups", reflect.TypeOf((*MockRepository)(nil).GetNewErrorGroups), arg0, arg1) } -// GetNewErrorGroupsCount mocks base method. -func (m *MockRepository) GetNewErrorGroupsCount(arg0 context.Context, arg1 types.GetErrorGroupsRequest) (uint64, error) { +// GetNewErrorGroupsTotal mocks base method. +func (m *MockRepository) GetNewErrorGroupsTotal(arg0 context.Context, arg1 types.GetErrorGroupsRequest) (uint64, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNewErrorGroupsCount", arg0, arg1) + ret := m.ctrl.Call(m, "GetNewErrorGroupsTotal", arg0, arg1) ret0, _ := ret[0].(uint64) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetNewErrorGroupsCount indicates an expected call of GetNewErrorGroupsCount. -func (mr *MockRepositoryMockRecorder) GetNewErrorGroupsCount(arg0, arg1 any) *gomock.Call { +// GetNewErrorGroupsTotal indicates an expected call of GetNewErrorGroupsTotal. +func (mr *MockRepositoryMockRecorder) GetNewErrorGroupsTotal(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNewErrorGroupsTotal", reflect.TypeOf((*MockRepository)(nil).GetNewErrorGroupsTotal), arg0, arg1) +} + +// GetReleases mocks base method. +func (m *MockRepository) GetReleases(arg0 context.Context, arg1 types.GetReleasesRequest) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetReleases", arg0, arg1) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetReleases indicates an expected call of GetReleases. +func (mr *MockRepositoryMockRecorder) GetReleases(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNewErrorGroupsCount", reflect.TypeOf((*MockRepository)(nil).GetNewErrorGroupsCount), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReleases", reflect.TypeOf((*MockRepository)(nil).GetReleases), arg0, arg1) } // GetServices mocks base method. @@ -205,3 +205,33 @@ func (mr *MockRepositoryMockRecorder) GetServices(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServices", reflect.TypeOf((*MockRepository)(nil).GetServices), arg0, arg1) } + +// GetTopErrorGroups mocks base method. +func (m *MockRepository) GetTopErrorGroups(arg0 context.Context, arg1 types.GetTopErrorGroupsRequest) ([]types.TopErrorGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTopErrorGroups", arg0, arg1) + ret0, _ := ret[0].([]types.TopErrorGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTopErrorGroups indicates an expected call of GetTopErrorGroups. +func (mr *MockRepositoryMockRecorder) GetTopErrorGroups(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTopErrorGroups", reflect.TypeOf((*MockRepository)(nil).GetTopErrorGroups), arg0, arg1) +} + +// GetTopErrorGroupsTotal mocks base method. +func (m *MockRepository) GetTopErrorGroupsTotal(arg0 context.Context, arg1 types.GetTopErrorGroupsRequest) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTopErrorGroupsTotal", arg0, arg1) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTopErrorGroupsTotal indicates an expected call of GetTopErrorGroupsTotal. +func (mr *MockRepositoryMockRecorder) GetTopErrorGroupsTotal(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTopErrorGroupsTotal", reflect.TypeOf((*MockRepository)(nil).GetTopErrorGroupsTotal), arg0, arg1) +} diff --git a/internal/pkg/repository_ch/repository.go b/internal/pkg/repository_ch/repository.go index 2a667f2..8b82723 100644 --- a/internal/pkg/repository_ch/repository.go +++ b/internal/pkg/repository_ch/repository.go @@ -14,14 +14,21 @@ import ( type Repository interface { GetErrorGroups(context.Context, types.GetErrorGroupsRequest) ([]types.ErrorGroup, error) - GetErrorGroupsCount(context.Context, types.GetErrorGroupsRequest) (uint64, error) + GetErrorGroupsTotal(context.Context, types.GetErrorGroupsRequest) (uint64, error) + GetNewErrorGroups(context.Context, types.GetErrorGroupsRequest) ([]types.ErrorGroup, error) - GetNewErrorGroupsCount(context.Context, types.GetErrorGroupsRequest) (uint64, error) + GetNewErrorGroupsTotal(context.Context, types.GetErrorGroupsRequest) (uint64, error) + + GetTopErrorGroups(context.Context, types.GetTopErrorGroupsRequest) ([]types.TopErrorGroup, error) + GetTopErrorGroupsTotal(context.Context, types.GetTopErrorGroupsRequest) (uint64, error) + GetErrorHist(context.Context, types.GetErrorHistRequest) ([]types.ErrorHistBucket, error) GetErrorDetails(context.Context, types.GetErrorGroupDetailsRequest) (types.ErrorGroupDetails, error) GetErrorCounts(context.Context, types.GetErrorGroupDetailsRequest) (types.ErrorGroupCounts, error) - GetErrorReleases(context.Context, types.GetErrorGroupReleasesRequest) ([]string, error) + GetServices(context.Context, types.GetServicesRequest) ([]string, error) + GetReleases(context.Context, types.GetReleasesRequest) ([]string, error) + DiffByReleases(context.Context, types.DiffByReleasesRequest) ([]types.DiffGroup, error) DiffByReleasesTotal(context.Context, types.DiffByReleasesRequest) (uint64, error) } diff --git a/internal/pkg/repository_ch/test.go b/internal/pkg/repository_ch/test.go new file mode 100644 index 0000000..5f7bb3e --- /dev/null +++ b/internal/pkg/repository_ch/test.go @@ -0,0 +1,136 @@ +package repositorych + +import ( + "testing" + "time" + + mock "github.com/ozontech/seq-ui/internal/pkg/repository_ch/mock" + "go.uber.org/mock/gomock" +) + +func fakeNow(now time.Time) func() time.Time { + return func() time.Time { + return now + } +} + +type mockConnRow struct { + query string + args []any + + scanErr error +} + +type mockRowsCount struct { + count int + scanErr error +} + +type mockRowsScan struct { + scanFns []func(...any) error + scanErr bool +} + +type mockRowsScanStruct struct { + scanStructFns []func(any) error + scanErr bool +} + +type mockConnRows struct { + query string + args []any + + rows any + err error +} + +func initMockConnRow(t *testing.T, params ...*mockConnRow) *mock.MockConn { + ctrl := gomock.NewController(t) + mc := mock.NewMockConn(ctrl) + + for _, p := range params { + if p == nil { + continue + } + + mockedRow := mock.NewMockRow(ctrl) + mockedRow.EXPECT().Scan(gomock.Any()).Return(p.scanErr) + + if p.query == "" { + mc.EXPECT(). + QueryRow(gomock.Any(), gomock.Any(), gomock.Any()). + Return(mockedRow). + Times(1) + } else { + mc.EXPECT(). + QueryRow(gomock.Any(), p.query, p.args...). + Return(mockedRow). + Times(1) + } + } + + return mc +} + +func initMockConnRows(t *testing.T, params ...*mockConnRows) *mock.MockConn { + ctrl := gomock.NewController(t) + mc := mock.NewMockConn(ctrl) + + for _, p := range params { + if p == nil { + continue + } + + mockedRows := mock.NewMockRows(ctrl) + + if p.rows != nil { + switch rows := (p.rows).(type) { + case *mockRowsCount: + times := rows.count + if rows.scanErr != nil { + times = 1 + } + + mockedRows.EXPECT().Next().Return(true).Times(times) + mockedRows.EXPECT().Scan(gomock.Any()).Return(rows.scanErr).Times(times) + + if rows.scanErr == nil { + mockedRows.EXPECT().Next().Return(false).Times(1) + } + case *mockRowsScan: + for _, fn := range rows.scanFns { + mockedRows.EXPECT().Next().Return(true) + mockedRows.EXPECT().Scan(gomock.Any()).DoAndReturn(fn) + } + + if !rows.scanErr { + mockedRows.EXPECT().Next().Return(false) + } + + case *mockRowsScanStruct: + for _, fn := range rows.scanStructFns { + mockedRows.EXPECT().Next().Return(true) + mockedRows.EXPECT().ScanStruct(gomock.Any()).DoAndReturn(fn) + } + + if !rows.scanErr { + mockedRows.EXPECT().Next().Return(false) + } + } + } + + if p.query == "" { + mc.EXPECT(). + Query(gomock.Any(), gomock.Any(), gomock.Any()). + Return(mockedRows, p.err). + Times(1) + } else { + mc.EXPECT(). + Query(gomock.Any(), p.query, p.args...). + Return(mockedRows, p.err). + Times(1) + } + } + + return mc +} diff --git a/internal/pkg/service/errorgroups/mock/service.go b/internal/pkg/service/errorgroups/mock/service.go new file mode 100644 index 0000000..6a1e9a5 --- /dev/null +++ b/internal/pkg/service/errorgroups/mock/service.go @@ -0,0 +1,166 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ozontech/seq-ui/internal/pkg/service/errorgroups (interfaces: Service) +// +// Generated by this command: +// +// mockgen -destination=internal/pkg/service/errorgroups/mock/service.go github.com/ozontech/seq-ui/internal/pkg/service/errorgroups Service +// + +// Package mock_errorgroups is a generated GoMock package. +package mock_errorgroups + +import ( + context "context" + reflect "reflect" + + types "github.com/ozontech/seq-ui/internal/app/types" + gomock "go.uber.org/mock/gomock" +) + +// MockService is a mock of Service interface. +type MockService struct { + ctrl *gomock.Controller + recorder *MockServiceMockRecorder + isgomock struct{} +} + +// MockServiceMockRecorder is the mock recorder for MockService. +type MockServiceMockRecorder struct { + mock *MockService +} + +// NewMockService creates a new mock instance. +func NewMockService(ctrl *gomock.Controller) *MockService { + mock := &MockService{ctrl: ctrl} + mock.recorder = &MockServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockService) EXPECT() *MockServiceMockRecorder { + return m.recorder +} + +// DiffByReleases mocks base method. +func (m *MockService) DiffByReleases(arg0 context.Context, arg1 types.DiffByReleasesRequest) ([]types.DiffGroup, uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DiffByReleases", arg0, arg1) + ret0, _ := ret[0].([]types.DiffGroup) + ret1, _ := ret[1].(uint64) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// DiffByReleases indicates an expected call of DiffByReleases. +func (mr *MockServiceMockRecorder) DiffByReleases(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DiffByReleases", reflect.TypeOf((*MockService)(nil).DiffByReleases), arg0, arg1) +} + +// GetDetails mocks base method. +func (m *MockService) GetDetails(arg0 context.Context, arg1 types.GetErrorGroupDetailsRequest) (types.ErrorGroupDetails, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDetails", arg0, arg1) + ret0, _ := ret[0].(types.ErrorGroupDetails) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDetails indicates an expected call of GetDetails. +func (mr *MockServiceMockRecorder) GetDetails(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDetails", reflect.TypeOf((*MockService)(nil).GetDetails), arg0, arg1) +} + +// GetErrorGroups mocks base method. +func (m *MockService) GetErrorGroups(arg0 context.Context, arg1 types.GetErrorGroupsRequest) ([]types.ErrorGroup, uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetErrorGroups", arg0, arg1) + ret0, _ := ret[0].([]types.ErrorGroup) + ret1, _ := ret[1].(uint64) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetErrorGroups indicates an expected call of GetErrorGroups. +func (mr *MockServiceMockRecorder) GetErrorGroups(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetErrorGroups", reflect.TypeOf((*MockService)(nil).GetErrorGroups), arg0, arg1) +} + +// GetHist mocks base method. +func (m *MockService) GetHist(arg0 context.Context, arg1 types.GetErrorHistRequest) ([]types.ErrorHistBucket, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetHist", arg0, arg1) + ret0, _ := ret[0].([]types.ErrorHistBucket) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHist indicates an expected call of GetHist. +func (mr *MockServiceMockRecorder) GetHist(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHist", reflect.TypeOf((*MockService)(nil).GetHist), arg0, arg1) +} + +// GetNewErrorGroups mocks base method. +func (m *MockService) GetNewErrorGroups(arg0 context.Context, arg1 types.GetErrorGroupsRequest) ([]types.ErrorGroup, uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNewErrorGroups", arg0, arg1) + ret0, _ := ret[0].([]types.ErrorGroup) + ret1, _ := ret[1].(uint64) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetNewErrorGroups indicates an expected call of GetNewErrorGroups. +func (mr *MockServiceMockRecorder) GetNewErrorGroups(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNewErrorGroups", reflect.TypeOf((*MockService)(nil).GetNewErrorGroups), arg0, arg1) +} + +// GetReleases mocks base method. +func (m *MockService) GetReleases(arg0 context.Context, arg1 types.GetReleasesRequest) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetReleases", arg0, arg1) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetReleases indicates an expected call of GetReleases. +func (mr *MockServiceMockRecorder) GetReleases(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReleases", reflect.TypeOf((*MockService)(nil).GetReleases), arg0, arg1) +} + +// GetServices mocks base method. +func (m *MockService) GetServices(arg0 context.Context, arg1 types.GetServicesRequest) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetServices", arg0, arg1) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetServices indicates an expected call of GetServices. +func (mr *MockServiceMockRecorder) GetServices(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServices", reflect.TypeOf((*MockService)(nil).GetServices), arg0, arg1) +} + +// GetTopErrorGroups mocks base method. +func (m *MockService) GetTopErrorGroups(arg0 context.Context, arg1 types.GetTopErrorGroupsRequest) ([]types.TopErrorGroup, uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTopErrorGroups", arg0, arg1) + ret0, _ := ret[0].([]types.TopErrorGroup) + ret1, _ := ret[1].(uint64) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetTopErrorGroups indicates an expected call of GetTopErrorGroups. +func (mr *MockServiceMockRecorder) GetTopErrorGroups(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTopErrorGroups", reflect.TypeOf((*MockService)(nil).GetTopErrorGroups), arg0, arg1) +} diff --git a/internal/pkg/service/errorgroups/service.go b/internal/pkg/service/errorgroups/service.go index f1d9d6c..3066154 100644 --- a/internal/pkg/service/errorgroups/service.go +++ b/internal/pkg/service/errorgroups/service.go @@ -17,26 +17,40 @@ const ( defaultLimit uint32 = 25 ) -type Service struct { +type Service interface { + GetErrorGroups(context.Context, types.GetErrorGroupsRequest) ([]types.ErrorGroup, uint64, error) + GetNewErrorGroups(context.Context, types.GetErrorGroupsRequest) ([]types.ErrorGroup, uint64, error) + GetTopErrorGroups(context.Context, types.GetTopErrorGroupsRequest) ([]types.TopErrorGroup, uint64, error) + + GetDetails(context.Context, types.GetErrorGroupDetailsRequest) (types.ErrorGroupDetails, error) + GetHist(context.Context, types.GetErrorHistRequest) ([]types.ErrorHistBucket, error) + + GetServices(context.Context, types.GetServicesRequest) ([]string, error) + GetReleases(context.Context, types.GetReleasesRequest) ([]string, error) + + DiffByReleases(context.Context, types.DiffByReleasesRequest) ([]types.DiffGroup, uint64, error) +} + +type service struct { repo repositorych.Repository logTagsMapping config.LogTagsMapping } -func New(repo repositorych.Repository, logTagsMapping config.LogTagsMapping) *Service { - return &Service{ +func New(repo repositorych.Repository, logTagsMapping config.LogTagsMapping) Service { + return &service{ repo: repo, logTagsMapping: logTagsMapping, } } -func (s *Service) GetErrorGroups( +func (s *service) GetErrorGroups( ctx context.Context, req types.GetErrorGroupsRequest, ) ([]types.ErrorGroup, uint64, error) { - return getErrorGroups(ctx, req, s.repo.GetErrorGroups, s.repo.GetErrorGroupsCount) + return getErrorGroups(ctx, req, s.repo.GetErrorGroups, s.repo.GetErrorGroupsTotal) } -func (s *Service) GetNewErrorGroups( +func (s *service) GetNewErrorGroups( ctx context.Context, req types.GetErrorGroupsRequest, ) ([]types.ErrorGroup, uint64, error) { @@ -48,7 +62,7 @@ func (s *Service) GetNewErrorGroups( return nil, 0, nil } - return getErrorGroups(ctx, req, s.repo.GetNewErrorGroups, s.repo.GetNewErrorGroupsCount) + return getErrorGroups(ctx, req, s.repo.GetNewErrorGroups, s.repo.GetNewErrorGroupsTotal) } func getErrorGroups( @@ -91,26 +105,53 @@ func getErrorGroups( return groups, total, err } -func (s *Service) GetHist( +func (s *service) GetTopErrorGroups( ctx context.Context, - req types.GetErrorHistRequest, -) ([]types.ErrorHistBucket, error) { - if req.Service == "" { - return nil, types.NewErrInvalidRequestField("'service' must not be empty") + req types.GetTopErrorGroupsRequest, +) ([]types.TopErrorGroup, uint64, error) { + if req.Limit == 0 { + req.Limit = defaultLimit } + eg, ctx := errgroup.WithContext(ctx) + + var groups []types.TopErrorGroup + eg.Go(func() error { + var err error + groups, err = s.repo.GetTopErrorGroups(ctx, req) + return err + }) + + var total uint64 + if req.WithTotal { + eg.Go(func() error { + var err error + total, err = s.repo.GetTopErrorGroupsTotal(ctx, req) + return err + }) + } + + err := eg.Wait() + if err != nil { + return nil, 0, fmt.Errorf("get top error groups failed: %w", err) + } + + return groups, total, err +} + +func (s *service) GetHist( + ctx context.Context, + req types.GetErrorHistRequest, +) ([]types.ErrorHistBucket, error) { return s.repo.GetErrorHist(ctx, req) } -func (s *Service) GetDetails( +func (s *service) GetDetails( ctx context.Context, req types.GetErrorGroupDetailsRequest, ) (types.ErrorGroupDetails, error) { details := types.ErrorGroupDetails{} - if req.Service == "" { - return details, types.NewErrInvalidRequestField("'service' must not be empty") - } if req.GroupHash == 0 { return details, types.NewErrInvalidRequestField("'group_hash' must not be empty") } @@ -121,37 +162,36 @@ func (s *Service) GetDetails( if err != nil { return details, fmt.Errorf("get error details failed: %w", err) } - - if details.SeenTotal > 0 { - details.Distributions = types.ErrorGroupDistributions{ - ByEnv: []types.ErrorGroupDistribution{{Value: *req.Env, Percent: 100}}, - ByRelease: []types.ErrorGroupDistribution{{Value: *req.Release, Percent: 100}}, - } - } return details, nil } - group, groupCtx := errgroup.WithContext(ctx) + eg, groupCtx := errgroup.WithContext(ctx) - group.Go(func() error { + eg.Go(func() error { var err error details, err = s.repo.GetErrorDetails(groupCtx, req) return err }) var counts types.ErrorGroupCounts - group.Go(func() error { + eg.Go(func() error { var err error counts, err = s.repo.GetErrorCounts(ctx, req) return err }) - err := group.Wait() + err := eg.Wait() if err != nil { return details, fmt.Errorf("get error details failed: %w", err) } - calcDistribution := func(count types.ErrorGroupCount) []types.ErrorGroupDistribution { + calcDistribution := func(count types.ErrorGroupCount, filter *string) []types.ErrorGroupDistribution { + // calculate distribution for unfiltered columns only. + // if filter is set, its distribution is always 100%. + if len(count) == 0 || (filter != nil && *filter != "") { + return nil + } + distr := make([]types.ErrorGroupDistribution, 0, len(count)) for v, c := range count { percent := (float64(c) / float64(details.SeenTotal)) * float64(100) @@ -166,9 +206,13 @@ func (s *Service) GetDetails( return distr } - details.Distributions = types.ErrorGroupDistributions{ - ByEnv: calcDistribution(counts.ByEnv), - ByRelease: calcDistribution(counts.ByRelease), + if details.SeenTotal > 0 { + details.Distributions = types.ErrorGroupDistributions{ + ByEnv: calcDistribution(counts.ByEnv, req.Env), + BySource: calcDistribution(counts.BySource, req.Source), + ByService: calcDistribution(counts.ByService, req.Service), + ByRelease: calcDistribution(counts.ByRelease, req.Release), + } } clearLogTags := func(filter *string, mapping []string) { @@ -180,31 +224,32 @@ func (s *Service) GetDetails( } // remove tags if they are not included in request - clearLogTags(req.Release, s.logTagsMapping.Release) clearLogTags(req.Env, s.logTagsMapping.Env) + clearLogTags(req.Service, s.logTagsMapping.Service) + clearLogTags(req.Release, s.logTagsMapping.Release) return details, nil } -func (s *Service) GetReleases( +func (s *service) GetServices( ctx context.Context, - req types.GetErrorGroupReleasesRequest, + req types.GetServicesRequest, ) ([]string, error) { - if req.Service == "" { - return nil, types.NewErrInvalidRequestField("'service' must not be empty") - } - - return s.repo.GetErrorReleases(ctx, req) + return s.repo.GetServices(ctx, req) } -func (s *Service) GetServices( +func (s *service) GetReleases( ctx context.Context, - req types.GetServicesRequest, + req types.GetReleasesRequest, ) ([]string, error) { - return s.repo.GetServices(ctx, req) + if req.Service == "" { + return nil, types.NewErrInvalidRequestField("'service' must not be empty") + } + + return s.repo.GetReleases(ctx, req) } -func (s *Service) DiffByReleases( +func (s *service) DiffByReleases( ctx context.Context, req types.DiffByReleasesRequest, ) ([]types.DiffGroup, uint64, error) { @@ -224,10 +269,10 @@ func (s *Service) DiffByReleases( eg, ctx := errgroup.WithContext(ctx) - var diffGroups []types.DiffGroup + var groups []types.DiffGroup eg.Go(func() error { var err error - diffGroups, err = s.repo.DiffByReleases(ctx, req) + groups, err = s.repo.DiffByReleases(ctx, req) return err }) @@ -245,5 +290,5 @@ func (s *Service) DiffByReleases( return nil, 0, fmt.Errorf("diff by releases failed: %w", err) } - return diffGroups, total, err + return groups, total, err } diff --git a/internal/pkg/service/errorgroups/service_test.go b/internal/pkg/service/errorgroups/service_test.go new file mode 100644 index 0000000..75e4ad6 --- /dev/null +++ b/internal/pkg/service/errorgroups/service_test.go @@ -0,0 +1,1262 @@ +package errorgroups + +import ( + "context" + "errors" + "slices" + "testing" + "time" + + "github.com/ozontech/seq-ui/internal/app/config" + "github.com/ozontech/seq-ui/internal/app/types" + mock "github.com/ozontech/seq-ui/internal/pkg/repository_ch/mock" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestGetErrorGroups(t *testing.T) { + var ( + service = "test-svc" + someErr = errors.New("some err") + ) + + type mockArgs struct { + req types.GetErrorGroupsRequest + + groupsCount int + errGroups error + + total uint64 + errTotal error + } + + tests := []struct { + name string + + req types.GetErrorGroupsRequest + wantGroupsCount int + wantTotal uint64 + wantErr bool + + mockArgs *mockArgs + }{ + { + name: "ok_limit", + + req: types.GetErrorGroupsRequest{ + Service: service, + Limit: 5, + }, + wantGroupsCount: 5, + wantTotal: 0, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupsRequest{ + Service: service, + Limit: 5, + }, + + groupsCount: 5, + }, + }, + { + name: "ok_no_limit", + + req: types.GetErrorGroupsRequest{ + Service: service, + }, + wantGroupsCount: 10, + wantTotal: 0, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupsRequest{ + Service: service, + Limit: defaultLimit, + }, + + groupsCount: 10, + }, + }, + { + name: "ok_with_total", + + req: types.GetErrorGroupsRequest{ + Service: service, + Limit: 10, + WithTotal: true, + }, + wantGroupsCount: 10, + wantTotal: 100, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupsRequest{ + Service: service, + Limit: 10, + WithTotal: true, + }, + + groupsCount: 10, + total: 100, + }, + }, + { + name: "err_no_service", + + req: types.GetErrorGroupsRequest{}, + wantErr: true, + }, + { + name: "err_repo_groups", + + req: types.GetErrorGroupsRequest{ + Service: service, + Limit: 10, + WithTotal: true, + }, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupsRequest{ + Service: service, + Limit: 10, + WithTotal: true, + }, + + errGroups: someErr, + total: 100, + }, + }, + { + name: "err_repo_total", + + req: types.GetErrorGroupsRequest{ + Service: service, + Limit: 10, + WithTotal: true, + }, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupsRequest{ + Service: service, + Limit: 10, + WithTotal: true, + }, + + groupsCount: 10, + errTotal: someErr, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockedRepo := mock.NewMockRepository(ctrl) + + svc := New(mockedRepo, config.LogTagsMapping{}) + + if ma := tt.mockArgs; ma != nil { + mockedRepo.EXPECT(). + GetErrorGroups(gomock.Any(), ma.req). + Return( + slices.Repeat([]types.ErrorGroup{{}}, ma.groupsCount), + ma.errGroups, + ). + Times(1) + + if tt.req.WithTotal { + mockedRepo.EXPECT(). + GetErrorGroupsTotal(gomock.Any(), ma.req). + Return(ma.total, ma.errTotal). + Times(1) + } + } + + gotGroups, gotTotal, err := svc.GetErrorGroups(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { + return + } + + require.Equal(t, tt.wantGroupsCount, len(gotGroups)) + require.Equal(t, tt.wantTotal, gotTotal) + }) + } +} + +func TestGetNewErrorGroups(t *testing.T) { + var ( + service = "test-svc" + release = "test-release" + duration = 24 * time.Hour + someErr = errors.New("some err") + ) + + type mockArgs struct { + req types.GetErrorGroupsRequest + + groupsCount int + errGroups error + + total uint64 + errTotal error + } + + tests := []struct { + name string + + req types.GetErrorGroupsRequest + wantGroupsCount int + wantTotal uint64 + wantErr bool + + mockArgs *mockArgs + }{ + { + name: "ok_empty_req", + + req: types.GetErrorGroupsRequest{}, + wantGroupsCount: 0, + wantTotal: 0, + }, + { + name: "ok_limit", + + req: types.GetErrorGroupsRequest{ + Service: service, + Release: &release, + Duration: &duration, + Limit: 5, + }, + wantGroupsCount: 5, + wantTotal: 0, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupsRequest{ + Service: service, + Release: &release, + Duration: &duration, + Limit: 5, + }, + + groupsCount: 5, + }, + }, + { + name: "ok_no_limit", + + req: types.GetErrorGroupsRequest{ + Service: service, + Release: &release, + Duration: &duration, + }, + wantGroupsCount: 10, + wantTotal: 0, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupsRequest{ + Service: service, + Release: &release, + Duration: &duration, + Limit: defaultLimit, + }, + + groupsCount: 10, + }, + }, + { + name: "ok_with_total", + + req: types.GetErrorGroupsRequest{ + Service: service, + Release: &release, + Duration: &duration, + Limit: 10, + WithTotal: true, + }, + wantGroupsCount: 10, + wantTotal: 100, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupsRequest{ + Service: service, + Release: &release, + Duration: &duration, + Limit: 10, + WithTotal: true, + }, + + groupsCount: 10, + total: 100, + }, + }, + { + name: "err_no_service", + + req: types.GetErrorGroupsRequest{ + Release: &release, + Duration: &duration, + }, + wantErr: true, + }, + { + name: "err_repo_groups", + + req: types.GetErrorGroupsRequest{ + Service: service, + Release: &release, + Duration: &duration, + Limit: 10, + WithTotal: true, + }, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupsRequest{ + Service: service, + Release: &release, + Duration: &duration, + Limit: 10, + WithTotal: true, + }, + + errGroups: someErr, + total: 100, + }, + }, + { + name: "err_repo_total", + + req: types.GetErrorGroupsRequest{ + Service: service, + Release: &release, + Duration: &duration, + Limit: 10, + WithTotal: true, + }, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupsRequest{ + Service: service, + Release: &release, + Duration: &duration, + Limit: 10, + WithTotal: true, + }, + + groupsCount: 10, + errTotal: someErr, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockedRepo := mock.NewMockRepository(ctrl) + + svc := New(mockedRepo, config.LogTagsMapping{}) + + if ma := tt.mockArgs; ma != nil { + mockedRepo.EXPECT(). + GetNewErrorGroups(gomock.Any(), ma.req). + Return( + slices.Repeat([]types.ErrorGroup{{}}, ma.groupsCount), + ma.errGroups, + ). + Times(1) + + if tt.req.WithTotal { + mockedRepo.EXPECT(). + GetNewErrorGroupsTotal(gomock.Any(), ma.req). + Return(ma.total, ma.errTotal). + Times(1) + } + } + + gotGroups, gotTotal, err := svc.GetNewErrorGroups(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { + return + } + + require.Equal(t, tt.wantGroupsCount, len(gotGroups)) + require.Equal(t, tt.wantTotal, gotTotal) + }) + } +} + +func TestGetTopErrorGroups(t *testing.T) { + var someErr = errors.New("some err") + + type mockArgs struct { + req types.GetTopErrorGroupsRequest + + groupsCount int + errGroups error + + total uint64 + errTotal error + } + + tests := []struct { + name string + + req types.GetTopErrorGroupsRequest + wantGroupsCount int + wantTotal uint64 + wantErr bool + + mockArgs *mockArgs + }{ + { + name: "ok_limit", + + req: types.GetTopErrorGroupsRequest{ + Limit: 5, + }, + wantGroupsCount: 5, + wantTotal: 0, + + mockArgs: &mockArgs{ + req: types.GetTopErrorGroupsRequest{ + Limit: 5, + }, + + groupsCount: 5, + }, + }, + { + name: "ok_no_limit", + + req: types.GetTopErrorGroupsRequest{}, + wantGroupsCount: 10, + wantTotal: 0, + + mockArgs: &mockArgs{ + req: types.GetTopErrorGroupsRequest{ + Limit: defaultLimit, + }, + + groupsCount: 10, + }, + }, + { + name: "ok_with_total", + + req: types.GetTopErrorGroupsRequest{ + Limit: 10, + WithTotal: true, + }, + wantGroupsCount: 10, + wantTotal: 100, + + mockArgs: &mockArgs{ + req: types.GetTopErrorGroupsRequest{ + Limit: 10, + WithTotal: true, + }, + + groupsCount: 10, + total: 100, + }, + }, + { + name: "err_repo_groups", + + req: types.GetTopErrorGroupsRequest{ + Limit: 10, + WithTotal: true, + }, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetTopErrorGroupsRequest{ + Limit: 10, + WithTotal: true, + }, + + errGroups: someErr, + total: 100, + }, + }, + { + name: "err_repo_total", + + req: types.GetTopErrorGroupsRequest{ + Limit: 10, + WithTotal: true, + }, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetTopErrorGroupsRequest{ + Limit: 10, + WithTotal: true, + }, + + groupsCount: 10, + errTotal: someErr, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockedRepo := mock.NewMockRepository(ctrl) + + svc := New(mockedRepo, config.LogTagsMapping{}) + + if ma := tt.mockArgs; ma != nil { + mockedRepo.EXPECT(). + GetTopErrorGroups(gomock.Any(), ma.req). + Return( + slices.Repeat([]types.TopErrorGroup{{}}, ma.groupsCount), + ma.errGroups, + ). + Times(1) + + if tt.req.WithTotal { + mockedRepo.EXPECT(). + GetTopErrorGroupsTotal(gomock.Any(), ma.req). + Return(ma.total, ma.errTotal). + Times(1) + } + } + + gotGroups, gotTotal, err := svc.GetTopErrorGroups(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { + return + } + + require.Equal(t, tt.wantGroupsCount, len(gotGroups)) + require.Equal(t, tt.wantTotal, gotTotal) + }) + } +} + +func TestGetHist(t *testing.T) { + var someErr = errors.New("some err") + + type mockArgs struct { + req types.GetErrorHistRequest + + bucketsCount int + err error + } + + tests := []struct { + name string + + req types.GetErrorHistRequest + wantBucketsCount int + wantErr bool + + mockArgs *mockArgs + }{ + { + name: "ok", + + req: types.GetErrorHistRequest{}, + wantBucketsCount: 50, + + mockArgs: &mockArgs{ + req: types.GetErrorHistRequest{}, + + bucketsCount: 50, + }, + }, + { + name: "err_repo", + + req: types.GetErrorHistRequest{}, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetErrorHistRequest{}, + + err: someErr, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockedRepo := mock.NewMockRepository(ctrl) + + svc := New(mockedRepo, config.LogTagsMapping{}) + + if ma := tt.mockArgs; ma != nil { + mockedRepo.EXPECT(). + GetErrorHist(gomock.Any(), ma.req). + Return( + slices.Repeat([]types.ErrorHistBucket{{}}, ma.bucketsCount), + ma.err, + ). + Times(1) + } + + got, err := svc.GetHist(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { + return + } + + require.Equal(t, tt.wantBucketsCount, len(got)) + }) + } +} + +func TestGetDetails(t *testing.T) { + var ( + hash = uint64(123) + env = "test-env" + source = "test-source" + service = "test-svc" + release = "test-release" + someErr = errors.New("some err") + ) + + type mockArgs struct { + req types.GetErrorGroupDetailsRequest + + details types.ErrorGroupDetails + errDetails error + + counts types.ErrorGroupCounts + errCounts error + } + + tests := []struct { + name string + + req types.GetErrorGroupDetailsRequest + want types.ErrorGroupDetails + wantErr bool + + logTagsMapping config.LogTagsMapping + + mockArgs *mockArgs + }{ + { + name: "ok_full", + + req: types.GetErrorGroupDetailsRequest{ + GroupHash: hash, + Env: &env, + Source: &source, + Service: &service, + Release: &release, + }, + want: types.ErrorGroupDetails{ + SeenTotal: 10, + }, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupDetailsRequest{ + GroupHash: hash, + Env: &env, + Source: &source, + Service: &service, + Release: &release, + }, + + details: types.ErrorGroupDetails{ + SeenTotal: 10, + }, + }, + }, + { + name: "ok_not_full", + + req: types.GetErrorGroupDetailsRequest{ + GroupHash: hash, + Env: &env, + Service: &service, + }, + want: types.ErrorGroupDetails{ + SeenTotal: 10, + Distributions: types.ErrorGroupDistributions{ + BySource: []types.ErrorGroupDistribution{ + {Value: "source2", Percent: 60}, + {Value: "source1", Percent: 40}, + }, + ByRelease: []types.ErrorGroupDistribution{ + {Value: "release1", Percent: 70}, + {Value: "release2", Percent: 30}, + }, + }, + }, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupDetailsRequest{ + GroupHash: hash, + Env: &env, + Service: &service, + }, + + details: types.ErrorGroupDetails{ + SeenTotal: 10, + }, + counts: types.ErrorGroupCounts{ + ByEnv: types.ErrorGroupCount{env: 10}, + ByService: types.ErrorGroupCount{service: 10}, + BySource: types.ErrorGroupCount{ + "source1": 4, + "source2": 6, + }, + ByRelease: types.ErrorGroupCount{ + "release1": 7, + "release2": 3, + }, + }, + }, + }, + { + name: "ok_not_full_seen_total_0", + + req: types.GetErrorGroupDetailsRequest{ + GroupHash: hash, + Env: &env, + Service: &service, + }, + want: types.ErrorGroupDetails{ + SeenTotal: 0, + }, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupDetailsRequest{ + GroupHash: hash, + Env: &env, + Service: &service, + }, + + details: types.ErrorGroupDetails{ + SeenTotal: 0, + }, + }, + }, + { + name: "ok_log_tags_mapping", + + req: types.GetErrorGroupDetailsRequest{ + GroupHash: hash, + }, + want: types.ErrorGroupDetails{ + LogTags: map[string]string{ + "hash": "123", + "source": source, + }, + }, + + logTagsMapping: config.LogTagsMapping{ + Env: []string{"env"}, + Service: []string{"service"}, + Release: []string{"release", "app_version"}, + }, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupDetailsRequest{ + GroupHash: hash, + }, + + details: types.ErrorGroupDetails{ + LogTags: map[string]string{ + "hash": "123", + "env": env, + "source": source, + "service": service, + "app_version": release, + }, + }, + }, + }, + { + name: "err_full_repo", + + req: types.GetErrorGroupDetailsRequest{ + GroupHash: hash, + Env: &env, + Source: &source, + Service: &service, + Release: &release, + }, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupDetailsRequest{ + GroupHash: hash, + Env: &env, + Source: &source, + Service: &service, + Release: &release, + }, + + errDetails: someErr, + }, + }, + { + name: "err_not_full_repo_details", + + req: types.GetErrorGroupDetailsRequest{ + GroupHash: hash, + Env: &env, + Service: &service, + }, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupDetailsRequest{ + GroupHash: hash, + Env: &env, + Service: &service, + }, + + errDetails: someErr, + }, + }, + { + name: "err_not_full_repo_counts", + + req: types.GetErrorGroupDetailsRequest{ + GroupHash: hash, + Env: &env, + Service: &service, + }, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetErrorGroupDetailsRequest{ + GroupHash: hash, + Env: &env, + Service: &service, + }, + + errCounts: someErr, + }, + }, + { + name: "err_no_hash", + + req: types.GetErrorGroupDetailsRequest{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockedRepo := mock.NewMockRepository(ctrl) + + svc := New(mockedRepo, tt.logTagsMapping) + + if ma := tt.mockArgs; ma != nil { + mockedRepo.EXPECT(). + GetErrorDetails(gomock.Any(), ma.req). + Return(ma.details, ma.errDetails). + Times(1) + + if !tt.req.IsFullyFilled() { + mockedRepo.EXPECT(). + GetErrorCounts(gomock.Any(), ma.req). + Return(ma.counts, ma.errCounts). + Times(1) + } + } + + got, err := svc.GetDetails(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { + return + } + + require.Equal(t, tt.want, got) + }) + } +} + +func TestGetServices(t *testing.T) { + var someErr = errors.New("some err") + + type mockArgs struct { + req types.GetServicesRequest + + services []string + err error + } + + tests := []struct { + name string + + req types.GetServicesRequest + wantServices []string + wantErr bool + + mockArgs *mockArgs + }{ + { + name: "ok", + + req: types.GetServicesRequest{}, + wantServices: []string{"service1", "service2"}, + + mockArgs: &mockArgs{ + req: types.GetServicesRequest{}, + + services: []string{"service1", "service2"}, + }, + }, + { + name: "err_repo", + + req: types.GetServicesRequest{}, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetServicesRequest{}, + + err: someErr, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockedRepo := mock.NewMockRepository(ctrl) + + svc := New(mockedRepo, config.LogTagsMapping{}) + + if ma := tt.mockArgs; ma != nil { + mockedRepo.EXPECT(). + GetServices(gomock.Any(), ma.req). + Return(ma.services, ma.err). + Times(1) + } + + got, err := svc.GetServices(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { + return + } + + require.Equal(t, tt.wantServices, got) + }) + } +} + +func TestGetReleases(t *testing.T) { + var ( + service = "test-svc" + someErr = errors.New("some err") + ) + + type mockArgs struct { + req types.GetReleasesRequest + + releases []string + err error + } + + tests := []struct { + name string + + req types.GetReleasesRequest + wantReleases []string + wantErr bool + + mockArgs *mockArgs + }{ + { + name: "ok", + + req: types.GetReleasesRequest{ + Service: service, + }, + wantReleases: []string{"release1", "release2"}, + + mockArgs: &mockArgs{ + req: types.GetReleasesRequest{ + Service: service, + }, + + releases: []string{"release1", "release2"}, + }, + }, + { + name: "err_no_service", + + req: types.GetReleasesRequest{}, + wantErr: true, + }, + { + name: "err_repo", + + req: types.GetReleasesRequest{ + Service: service, + }, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.GetReleasesRequest{ + Service: service, + }, + + err: someErr, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockedRepo := mock.NewMockRepository(ctrl) + + svc := New(mockedRepo, config.LogTagsMapping{}) + + if ma := tt.mockArgs; ma != nil { + mockedRepo.EXPECT(). + GetReleases(gomock.Any(), ma.req). + Return(ma.releases, ma.err). + Times(1) + } + + got, err := svc.GetReleases(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { + return + } + + require.Equal(t, tt.wantReleases, got) + }) + } +} + +func TestDiffByReleases(t *testing.T) { + var ( + service = "test-svc" + releases = []string{"release1", "release2"} + someErr = errors.New("some err") + ) + + type mockArgs struct { + req types.DiffByReleasesRequest + + groupsCount int + errGroups error + + total uint64 + errTotal error + } + + tests := []struct { + name string + + req types.DiffByReleasesRequest + wantGroupsCount int + wantTotal uint64 + wantErr bool + + mockArgs *mockArgs + }{ + { + name: "ok_limit", + + req: types.DiffByReleasesRequest{ + Service: service, + Releases: releases, + Limit: 5, + }, + wantGroupsCount: 5, + wantTotal: 0, + + mockArgs: &mockArgs{ + req: types.DiffByReleasesRequest{ + Service: service, + Releases: releases, + Limit: 5, + }, + + groupsCount: 5, + }, + }, + { + name: "ok_no_limit", + + req: types.DiffByReleasesRequest{ + Service: service, + Releases: releases, + }, + wantGroupsCount: 10, + wantTotal: 0, + + mockArgs: &mockArgs{ + req: types.DiffByReleasesRequest{ + Service: service, + Releases: releases, + Limit: defaultLimit, + }, + + groupsCount: 10, + }, + }, + { + name: "ok_with_total", + + req: types.DiffByReleasesRequest{ + Service: service, + Releases: releases, + Limit: 10, + WithTotal: true, + }, + wantGroupsCount: 10, + wantTotal: 100, + + mockArgs: &mockArgs{ + req: types.DiffByReleasesRequest{ + Service: service, + Releases: releases, + Limit: 10, + WithTotal: true, + }, + + groupsCount: 10, + total: 100, + }, + }, + { + name: "err_no_service", + + req: types.DiffByReleasesRequest{}, + wantErr: true, + }, + { + name: "err_not_enough_releases", + + req: types.DiffByReleasesRequest{ + Service: service, + Releases: []string{"release1"}, + }, + wantErr: true, + }, + { + name: "err_empty_release", + + req: types.DiffByReleasesRequest{ + Service: service, + Releases: []string{"release1", ""}, + }, + wantErr: true, + }, + { + name: "err_repo_groups", + + req: types.DiffByReleasesRequest{ + Service: service, + Releases: releases, + Limit: 10, + WithTotal: true, + }, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.DiffByReleasesRequest{ + Service: service, + Releases: releases, + Limit: 10, + WithTotal: true, + }, + + errGroups: someErr, + total: 100, + }, + }, + { + name: "err_repo_total", + + req: types.DiffByReleasesRequest{ + Service: service, + Releases: releases, + Limit: 10, + WithTotal: true, + }, + wantErr: true, + + mockArgs: &mockArgs{ + req: types.DiffByReleasesRequest{ + Service: service, + Releases: releases, + Limit: 10, + WithTotal: true, + }, + + groupsCount: 10, + errTotal: someErr, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockedRepo := mock.NewMockRepository(ctrl) + + svc := New(mockedRepo, config.LogTagsMapping{}) + + if ma := tt.mockArgs; ma != nil { + mockedRepo.EXPECT(). + DiffByReleases(gomock.Any(), ma.req). + Return( + slices.Repeat([]types.DiffGroup{{}}, ma.groupsCount), + ma.errGroups, + ). + Times(1) + + if tt.req.WithTotal { + mockedRepo.EXPECT(). + DiffByReleasesTotal(gomock.Any(), ma.req). + Return(ma.total, ma.errTotal). + Times(1) + } + } + + gotGroups, gotTotal, err := svc.DiffByReleases(context.Background(), tt.req) + + require.Equal(t, tt.wantErr, err != nil) + if tt.wantErr { + return + } + + require.Equal(t, tt.wantGroupsCount, len(gotGroups)) + require.Equal(t, tt.wantTotal, gotTotal) + }) + } +} diff --git a/migration_ch/1_initial.sql b/migration_ch/1_initial.sql index d9c1618..64b5ecf 100644 --- a/migration_ch/1_initial.sql +++ b/migration_ch/1_initial.sql @@ -12,7 +12,7 @@ CREATE TABLE IF NOT EXISTS seq_ui_server.events_raw log_tags Map(String, String), ttl DateTime DEFAULT now() ) -ENGINE MergeTree() +ENGINE = MergeTree() PARTITION BY toStartOfTenMinutes(timestamp) ORDER BY (timestamp) TTL ttl + INTERVAL 10 MINUTES @@ -53,7 +53,8 @@ AS SELECT FROM seq_ui_server.events_raw GROUP BY _group_hash, service, env, release, source, cluster; -CREATE TABLE IF NOT EXISTS seq_ui_server.agg_events_10min ( +CREATE TABLE IF NOT EXISTS seq_ui_server.agg_events_10min +( start_date DateTime NOT NULL, service String, _group_hash UInt64, @@ -82,13 +83,15 @@ AS SELECT FROM seq_ui_server.events_raw GROUP BY start_date, _group_hash, service, env, release, source, cluster; -CREATE TABLE IF NOT EXISTS seq_ui_server.services ( +CREATE TABLE IF NOT EXISTS seq_ui_server.services +( env LowCardinality(String), cluster LowCardinality(String), service String, release String, ttl DateTime -) ENGINE = ReplacingMergeTree() +) +ENGINE = ReplacingMergeTree() ORDER BY (cluster, env, service, release); CREATE MATERIALIZED VIEW IF NOT EXISTS seq_ui_server.services_mv TO seq_ui_server.services @@ -103,8 +106,12 @@ GROUP BY cluster, env, service, release; -- +goose Down DROP TABLE IF EXISTS seq_ui_server.events_raw; -DROP TABLE IF EXISTS seq_ui_server.error_groups; + DROP TABLE IF EXISTS seq_ui_server.error_groups_mv; +DROP TABLE IF EXISTS seq_ui_server.error_groups; + +DROP TABLE IF EXISTS seq_ui_server.agg_events_10min_mv; DROP TABLE IF EXISTS seq_ui_server.agg_events_10min; -DROP TABLE IF EXISTS seq_ui_server.services; + DROP TABLE IF EXISTS seq_ui_server.services_mv; +DROP TABLE IF EXISTS seq_ui_server.services; diff --git a/migration_ch/2_brief.sql b/migration_ch/2_brief.sql new file mode 100644 index 0000000..54102ef --- /dev/null +++ b/migration_ch/2_brief.sql @@ -0,0 +1,67 @@ +-- +goose Up +CREATE TABLE IF NOT EXISTS seq_ui_server.error_groups_brief +( + _group_hash UInt64, + cluster LowCardinality(String), + source LowCardinality(String), + env LowCardinality(String), + seen_total AggregateFunction(count), + ttl DateTime +) +ENGINE = AggregatingMergeTree() +ORDER BY (cluster, source, env, _group_hash) +TTL ttl + INTERVAL 3 MONTH; + +CREATE MATERIALIZED VIEW IF NOT EXISTS seq_ui_server.error_groups_brief_mv TO seq_ui_server.error_groups_brief +AS SELECT + _group_hash, + cluster, + source, + env, + countState() as seen_total, + max(timestamp) as ttl +FROM seq_ui_server.events_raw +GROUP BY cluster, source, env, _group_hash; + +CREATE TABLE IF NOT EXISTS seq_ui_server.agg_events_1d +( + start_date DateTime NOT NULL, + service String, + _group_hash UInt64, + env LowCardinality(String), + source LowCardinality(String), + cluster LowCardinality(String), + release String, + counts AggregateFunction(count) +) +ENGINE = AggregatingMergeTree() +PARTITION BY start_date +ORDER BY (cluster, source, env, service, release, _group_hash, start_date) +TTL start_date + INTERVAL 2 YEAR +SETTINGS ttl_only_drop_parts = 1, merge_with_ttl_timeout = 1800; + +CREATE MATERIALIZED VIEW IF NOT EXISTS seq_ui_server.agg_events_1d_mv TO seq_ui_server.agg_events_1d +AS SELECT + toStartOfDay(start_date) as start_date, + service, + _group_hash, + env, + source, + cluster, + release, + countMergeState(counts) AS counts +FROM seq_ui_server.agg_events_10min +GROUP BY cluster, source, env, service, release, _group_hash, start_date; + +ALTER TABLE seq_ui_server.error_groups MODIFY TTL ttl + INTERVAL 3 MONTH; +ALTER TABLE seq_ui_server.services MODIFY TTL ttl + INTERVAL 3 MONTH; + +-- +goose Down +DROP TABLE IF EXISTS seq_ui_server.error_groups_brief_mv; +DROP TABLE IF EXISTS seq_ui_server.error_groups_brief; + +DROP TABLE IF EXISTS seq_ui_server.agg_events_1d_mv; +DROP TABLE IF EXISTS seq_ui_server.agg_events_1d; + +ALTER TABLE seq_ui_server.error_groups REMOVE TTL; +ALTER TABLE seq_ui_server.services REMOVE TTL; diff --git a/migration_ch/sharded/1_initial.sql b/migration_ch/sharded/1_initial.sql index c2fa688..0119dfa 100644 --- a/migration_ch/sharded/1_initial.sql +++ b/migration_ch/sharded/1_initial.sql @@ -12,7 +12,7 @@ CREATE TABLE IF NOT EXISTS seq_ui_server_replicated.sharded_events_raw log_tags Map(String, String), ttl DateTime DEFAULT now() ) -ENGINE MergeTree() +ENGINE = MergeTree() PARTITION BY toStartOfTenMinutes(timestamp) ORDER BY (timestamp) TTL ttl + INTERVAL 10 MINUTES @@ -32,7 +32,8 @@ CREATE TABLE IF NOT EXISTS seq_ui_server_replicated.sharded_error_groups last_seen_at AggregateFunction(max, DateTime), log_tags Map(String, String), ttl DateTime -) ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/{shard}/{table}', '{replica}') +) +ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/{shard}/{table}', '{replica}') ORDER BY (cluster, source, env, service, release, _group_hash); CREATE MATERIALIZED VIEW IF NOT EXISTS seq_ui_server_replicated.sharded_error_groups_mv TO seq_ui_server_replicated.sharded_error_groups @@ -52,7 +53,8 @@ AS SELECT FROM seq_ui_server_replicated.sharded_events_raw GROUP BY _group_hash, service, env, release, source, cluster; -CREATE TABLE IF NOT EXISTS seq_ui_server_replicated.sharded_agg_events_10min ( +CREATE TABLE IF NOT EXISTS seq_ui_server_replicated.sharded_agg_events_10min +( start_date DateTime NOT NULL, service String, _group_hash UInt64, @@ -61,7 +63,8 @@ CREATE TABLE IF NOT EXISTS seq_ui_server_replicated.sharded_agg_events_10min ( cluster LowCardinality(String), release String, counts AggregateFunction(count) -) ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/{shard}/{table}', '{replica}') +) +ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/{shard}/{table}', '{replica}') PARTITION BY toStartOfHour(start_date) ORDER BY (cluster, source, env, service, release, _group_hash, start_date) TTL start_date + INTERVAL 3 MONTH @@ -80,13 +83,15 @@ AS SELECT FROM seq_ui_server_replicated.sharded_events_raw GROUP BY start_date, _group_hash, service, env, release, source, cluster; -CREATE TABLE IF NOT EXISTS seq_ui_server_replicated.sharded_services ( +CREATE TABLE IF NOT EXISTS seq_ui_server_replicated.sharded_services +( env LowCardinality(String), cluster LowCardinality(String), service String, release String, ttl DateTime -) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{shard}/{table}', '{replica}') +) +ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{shard}/{table}', '{replica}') ORDER BY (cluster, env, service, release); CREATE MATERIALIZED VIEW IF NOT EXISTS seq_ui_server_replicated.sharded_services_mv TO seq_ui_server_replicated.sharded_services @@ -105,12 +110,15 @@ CREATE TABLE seq_ui_server_replicated.services AS seq_ui_server_replicated.shard -- +goose Down DROP TABLE IF EXISTS seq_ui_server_replicated.sharded_events_raw; -DROP TABLE IF EXISTS seq_ui_server_replicated.sharded_error_groups; + DROP TABLE IF EXISTS seq_ui_server_replicated.sharded_error_groups_mv; -DROP TABLE IF EXISTS seq_ui_server_replicated.sharded_agg_events_10min; +DROP TABLE IF EXISTS seq_ui_server_replicated.sharded_error_groups; +DROP TABLE IF EXISTS seq_ui_server_replicated.error_groups; + DROP TABLE IF EXISTS seq_ui_server_replicated.sharded_agg_events_10min_mv; -DROP TABLE IF EXISTS seq_ui_server_replicated.sharded_services; -DROP TABLE IF EXISTS seq_ui_server_replicated.sharded_services_mv; +DROP TABLE IF EXISTS seq_ui_server_replicated.sharded_agg_events_10min; DROP TABLE IF EXISTS seq_ui_server_replicated.agg_events_10min; -DROP TABLE IF EXISTS seq_ui_server_replicated.error_groups; + +DROP TABLE IF EXISTS seq_ui_server_replicated.sharded_services_mv; +DROP TABLE IF EXISTS seq_ui_server_replicated.sharded_services; DROP TABLE IF EXISTS seq_ui_server_replicated.services; diff --git a/migration_ch/sharded/2_brief.sql b/migration_ch/sharded/2_brief.sql new file mode 100644 index 0000000..c8226e6 --- /dev/null +++ b/migration_ch/sharded/2_brief.sql @@ -0,0 +1,72 @@ +-- +goose Up +CREATE TABLE IF NOT EXISTS seq_ui_server_replicated.sharded_error_groups_brief +( + _group_hash UInt64, + cluster LowCardinality(String), + source LowCardinality(String), + env LowCardinality(String), + seen_total AggregateFunction(count), + ttl DateTime +) +ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/{shard}/{table}', '{replica}') +ORDER BY (cluster, source, env, _group_hash) +TTL ttl + INTERVAL 3 MONTH; + +CREATE MATERIALIZED VIEW IF NOT EXISTS seq_ui_server_replicated.sharded_error_groups_brief_mv TO seq_ui_server_replicated.sharded_error_groups_brief +AS SELECT + _group_hash, + cluster, + source, + env, + countState() as seen_total, + max(timestamp) as ttl +FROM seq_ui_server_replicated.sharded_events_raw +GROUP BY cluster, source, env, _group_hash; + +CREATE TABLE IF NOT EXISTS seq_ui_server_replicated.sharded_agg_events_1d +( + start_date DateTime NOT NULL, + service String, + _group_hash UInt64, + env LowCardinality(String), + source LowCardinality(String), + cluster LowCardinality(String), + release String, + counts AggregateFunction(count) +) +ENGINE = ReplicatedAggregatingMergeTree('/clickhouse/tables/{shard}/{table}', '{replica}') +PARTITION BY start_date +ORDER BY (cluster, source, env, service, release, _group_hash, start_date) +TTL start_date + INTERVAL 2 YEAR +SETTINGS ttl_only_drop_parts = 1, merge_with_ttl_timeout = 1800; + +CREATE MATERIALIZED VIEW IF NOT EXISTS seq_ui_server_replicated.sharded_agg_events_1d_mv TO seq_ui_server_replicated.sharded_agg_events_1d +AS SELECT + toStartOfDay(start_date) as start_date, + service, + _group_hash, + env, + source, + cluster, + release, + countMergeState(counts) AS counts +FROM seq_ui_server_replicated.sharded_agg_events_10min +GROUP BY cluster, source, env, service, release, _group_hash, start_date; + +CREATE TABLE seq_ui_server_replicated.error_groups_brief AS seq_ui_server_replicated.sharded_error_groups_brief ENGINE = Distributed("seq-ui-server-replicated", seq_ui_server_replicated, sharded_error_groups_brief); +CREATE TABLE seq_ui_server_replicated.agg_events_1d AS seq_ui_server_replicated.sharded_agg_events_1d ENGINE = Distributed("seq-ui-server-replicated", seq_ui_server_replicated, sharded_agg_events_1d); + +ALTER TABLE seq_ui_server_replicated.sharded_error_groups MODIFY TTL ttl + INTERVAL 3 MONTH; +ALTER TABLE seq_ui_server_replicated.sharded_services MODIFY TTL ttl + INTERVAL 3 MONTH; + +-- +goose Down +DROP TABLE IF EXISTS seq_ui_server_replicated.sharded_error_groups_brief_mv; +DROP TABLE IF EXISTS seq_ui_server_replicated.sharded_error_groups_brief; +DROP TABLE IF EXISTS seq_ui_server_replicated.error_groups_brief; + +DROP TABLE IF EXISTS seq_ui_server_replicated.sharded_agg_events_1d_mv; +DROP TABLE IF EXISTS seq_ui_server_replicated.sharded_agg_events_1d; +DROP TABLE IF EXISTS seq_ui_server_replicated.agg_events_1d; + +ALTER TABLE seq_ui_server_replicated.sharded_error_groups REMOVE TTL; +ALTER TABLE seq_ui_server_replicated.sharded_services REMOVE TTL; diff --git a/pkg/errorgroups/v1/errorgroups.pb.go b/pkg/errorgroups/v1/errorgroups.pb.go index 220df28..595e7ed 100644 --- a/pkg/errorgroups/v1/errorgroups.pb.go +++ b/pkg/errorgroups/v1/errorgroups.pb.go @@ -195,8 +195,8 @@ type GetGroupsResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Total uint64 `protobuf:"varint,1,opt,name=total,proto3" json:"total,omitempty"` - Groups []*Group `protobuf:"bytes,2,rep,name=groups,proto3" json:"groups,omitempty"` + Total uint64 `protobuf:"varint,1,opt,name=total,proto3" json:"total,omitempty"` + Groups []*GetGroupsResponse_Group `protobuf:"bytes,2,rep,name=groups,proto3" json:"groups,omitempty"` } func (x *GetGroupsResponse) Reset() { @@ -238,28 +238,28 @@ func (x *GetGroupsResponse) GetTotal() uint64 { return 0 } -func (x *GetGroupsResponse) GetGroups() []*Group { +func (x *GetGroupsResponse) GetGroups() []*GetGroupsResponse_Group { if x != nil { return x.Groups } return nil } -type Group struct { +type GetTopGroupsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Hash uint64 `protobuf:"varint,1,opt,name=hash,proto3" json:"hash,omitempty"` - Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` - SeenTotal uint64 `protobuf:"varint,3,opt,name=seen_total,json=seenTotal,proto3" json:"seen_total,omitempty"` - FirstSeenAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=first_seen_at,json=firstSeenAt,proto3" json:"first_seen_at,omitempty"` - LastSeenAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=last_seen_at,json=lastSeenAt,proto3" json:"last_seen_at,omitempty"` - Source string `protobuf:"bytes,6,opt,name=source,proto3" json:"source,omitempty"` + Env *string `protobuf:"bytes,1,opt,name=env,proto3,oneof" json:"env,omitempty"` + Source *string `protobuf:"bytes,2,opt,name=source,proto3,oneof" json:"source,omitempty"` + Duration *durationpb.Duration `protobuf:"bytes,3,opt,name=duration,proto3" json:"duration,omitempty"` + Limit uint32 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"` + Offset uint32 `protobuf:"varint,5,opt,name=offset,proto3" json:"offset,omitempty"` + WithTotal bool `protobuf:"varint,6,opt,name=with_total,json=withTotal,proto3" json:"with_total,omitempty"` } -func (x *Group) Reset() { - *x = Group{} +func (x *GetTopGroupsRequest) Reset() { + *x = GetTopGroupsRequest{} if protoimpl.UnsafeEnabled { mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -267,13 +267,13 @@ func (x *Group) Reset() { } } -func (x *Group) String() string { +func (x *GetTopGroupsRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Group) ProtoMessage() {} +func (*GetTopGroupsRequest) ProtoMessage() {} -func (x *Group) ProtoReflect() protoreflect.Message { +func (x *GetTopGroupsRequest) ProtoReflect() protoreflect.Message { mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -285,51 +285,106 @@ func (x *Group) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Group.ProtoReflect.Descriptor instead. -func (*Group) Descriptor() ([]byte, []int) { +// Deprecated: Use GetTopGroupsRequest.ProtoReflect.Descriptor instead. +func (*GetTopGroupsRequest) Descriptor() ([]byte, []int) { return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{2} } -func (x *Group) GetHash() uint64 { +func (x *GetTopGroupsRequest) GetEnv() string { + if x != nil && x.Env != nil { + return *x.Env + } + return "" +} + +func (x *GetTopGroupsRequest) GetSource() string { + if x != nil && x.Source != nil { + return *x.Source + } + return "" +} + +func (x *GetTopGroupsRequest) GetDuration() *durationpb.Duration { if x != nil { - return x.Hash + return x.Duration } - return 0 + return nil } -func (x *Group) GetMessage() string { +func (x *GetTopGroupsRequest) GetLimit() uint32 { if x != nil { - return x.Message + return x.Limit } - return "" + return 0 } -func (x *Group) GetSeenTotal() uint64 { +func (x *GetTopGroupsRequest) GetOffset() uint32 { if x != nil { - return x.SeenTotal + return x.Offset } return 0 } -func (x *Group) GetFirstSeenAt() *timestamppb.Timestamp { +func (x *GetTopGroupsRequest) GetWithTotal() bool { if x != nil { - return x.FirstSeenAt + return x.WithTotal + } + return false +} + +type GetTopGroupsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Total uint64 `protobuf:"varint,1,opt,name=total,proto3" json:"total,omitempty"` + Groups []*GetTopGroupsResponse_Group `protobuf:"bytes,2,rep,name=groups,proto3" json:"groups,omitempty"` +} + +func (x *GetTopGroupsResponse) Reset() { + *x = GetTopGroupsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } - return nil } -func (x *Group) GetLastSeenAt() *timestamppb.Timestamp { +func (x *GetTopGroupsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTopGroupsResponse) ProtoMessage() {} + +func (x *GetTopGroupsResponse) ProtoReflect() protoreflect.Message { + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetTopGroupsResponse.ProtoReflect.Descriptor instead. +func (*GetTopGroupsResponse) Descriptor() ([]byte, []int) { + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{3} +} + +func (x *GetTopGroupsResponse) GetTotal() uint64 { if x != nil { - return x.LastSeenAt + return x.Total } - return nil + return 0 } -func (x *Group) GetSource() string { +func (x *GetTopGroupsResponse) GetGroups() []*GetTopGroupsResponse_Group { if x != nil { - return x.Source + return x.Groups } - return "" + return nil } type GetHistRequest struct { @@ -337,7 +392,7 @@ type GetHistRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + Service *string `protobuf:"bytes,1,opt,name=service,proto3,oneof" json:"service,omitempty"` GroupHash *uint64 `protobuf:"varint,2,opt,name=group_hash,json=groupHash,proto3,oneof" json:"group_hash,omitempty"` Env *string `protobuf:"bytes,3,opt,name=env,proto3,oneof" json:"env,omitempty"` Release *string `protobuf:"bytes,4,opt,name=release,proto3,oneof" json:"release,omitempty"` @@ -348,7 +403,7 @@ type GetHistRequest struct { func (x *GetHistRequest) Reset() { *x = GetHistRequest{} if protoimpl.UnsafeEnabled { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[3] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -361,7 +416,7 @@ func (x *GetHistRequest) String() string { func (*GetHistRequest) ProtoMessage() {} func (x *GetHistRequest) ProtoReflect() protoreflect.Message { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[3] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -374,12 +429,12 @@ func (x *GetHistRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetHistRequest.ProtoReflect.Descriptor instead. func (*GetHistRequest) Descriptor() ([]byte, []int) { - return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{3} + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{4} } func (x *GetHistRequest) GetService() string { - if x != nil { - return x.Service + if x != nil && x.Service != nil { + return *x.Service } return "" } @@ -430,7 +485,7 @@ type GetHistResponse struct { func (x *GetHistResponse) Reset() { *x = GetHistResponse{} if protoimpl.UnsafeEnabled { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[4] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -443,7 +498,7 @@ func (x *GetHistResponse) String() string { func (*GetHistResponse) ProtoMessage() {} func (x *GetHistResponse) ProtoReflect() protoreflect.Message { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[4] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -456,7 +511,7 @@ func (x *GetHistResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetHistResponse.ProtoReflect.Descriptor instead. func (*GetHistResponse) Descriptor() ([]byte, []int) { - return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{4} + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{5} } func (x *GetHistResponse) GetBuckets() []*Bucket { @@ -478,7 +533,7 @@ type Bucket struct { func (x *Bucket) Reset() { *x = Bucket{} if protoimpl.UnsafeEnabled { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[5] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -491,7 +546,7 @@ func (x *Bucket) String() string { func (*Bucket) ProtoMessage() {} func (x *Bucket) ProtoReflect() protoreflect.Message { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[5] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -504,7 +559,7 @@ func (x *Bucket) ProtoReflect() protoreflect.Message { // Deprecated: Use Bucket.ProtoReflect.Descriptor instead. func (*Bucket) Descriptor() ([]byte, []int) { - return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{5} + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{6} } func (x *Bucket) GetTime() *timestamppb.Timestamp { @@ -526,7 +581,7 @@ type GetDetailsRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + Service *string `protobuf:"bytes,1,opt,name=service,proto3,oneof" json:"service,omitempty"` GroupHash uint64 `protobuf:"varint,2,opt,name=group_hash,json=groupHash,proto3" json:"group_hash,omitempty"` Env *string `protobuf:"bytes,3,opt,name=env,proto3,oneof" json:"env,omitempty"` Release *string `protobuf:"bytes,4,opt,name=release,proto3,oneof" json:"release,omitempty"` @@ -536,7 +591,7 @@ type GetDetailsRequest struct { func (x *GetDetailsRequest) Reset() { *x = GetDetailsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[6] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -549,7 +604,7 @@ func (x *GetDetailsRequest) String() string { func (*GetDetailsRequest) ProtoMessage() {} func (x *GetDetailsRequest) ProtoReflect() protoreflect.Message { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[6] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -562,12 +617,12 @@ func (x *GetDetailsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDetailsRequest.ProtoReflect.Descriptor instead. func (*GetDetailsRequest) Descriptor() ([]byte, []int) { - return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{6} + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{7} } func (x *GetDetailsRequest) GetService() string { - if x != nil { - return x.Service + if x != nil && x.Service != nil { + return *x.Service } return "" } @@ -618,7 +673,7 @@ type GetDetailsResponse struct { func (x *GetDetailsResponse) Reset() { *x = GetDetailsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[7] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -631,7 +686,7 @@ func (x *GetDetailsResponse) String() string { func (*GetDetailsResponse) ProtoMessage() {} func (x *GetDetailsResponse) ProtoReflect() protoreflect.Message { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[7] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -644,7 +699,7 @@ func (x *GetDetailsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDetailsResponse.ProtoReflect.Descriptor instead. func (*GetDetailsResponse) Descriptor() ([]byte, []int) { - return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{7} + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{8} } func (x *GetDetailsResponse) GetGroupHash() uint64 { @@ -715,7 +770,7 @@ type GetReleasesRequest struct { func (x *GetReleasesRequest) Reset() { *x = GetReleasesRequest{} if protoimpl.UnsafeEnabled { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[8] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -728,7 +783,7 @@ func (x *GetReleasesRequest) String() string { func (*GetReleasesRequest) ProtoMessage() {} func (x *GetReleasesRequest) ProtoReflect() protoreflect.Message { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[8] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -741,7 +796,7 @@ func (x *GetReleasesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetReleasesRequest.ProtoReflect.Descriptor instead. func (*GetReleasesRequest) Descriptor() ([]byte, []int) { - return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{8} + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{9} } func (x *GetReleasesRequest) GetService() string { @@ -769,7 +824,7 @@ type GetReleasesResponse struct { func (x *GetReleasesResponse) Reset() { *x = GetReleasesResponse{} if protoimpl.UnsafeEnabled { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[9] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -782,7 +837,7 @@ func (x *GetReleasesResponse) String() string { func (*GetReleasesResponse) ProtoMessage() {} func (x *GetReleasesResponse) ProtoReflect() protoreflect.Message { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[9] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -795,7 +850,7 @@ func (x *GetReleasesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetReleasesResponse.ProtoReflect.Descriptor instead. func (*GetReleasesResponse) Descriptor() ([]byte, []int) { - return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{9} + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{10} } func (x *GetReleasesResponse) GetReleases() []string { @@ -819,7 +874,7 @@ type GetServicesRequest struct { func (x *GetServicesRequest) Reset() { *x = GetServicesRequest{} if protoimpl.UnsafeEnabled { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[10] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -832,7 +887,7 @@ func (x *GetServicesRequest) String() string { func (*GetServicesRequest) ProtoMessage() {} func (x *GetServicesRequest) ProtoReflect() protoreflect.Message { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[10] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -845,7 +900,7 @@ func (x *GetServicesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetServicesRequest.ProtoReflect.Descriptor instead. func (*GetServicesRequest) Descriptor() ([]byte, []int) { - return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{10} + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{11} } func (x *GetServicesRequest) GetQuery() string { @@ -887,7 +942,7 @@ type GetServicesResponse struct { func (x *GetServicesResponse) Reset() { *x = GetServicesResponse{} if protoimpl.UnsafeEnabled { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[11] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -900,7 +955,7 @@ func (x *GetServicesResponse) String() string { func (*GetServicesResponse) ProtoMessage() {} func (x *GetServicesResponse) ProtoReflect() protoreflect.Message { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[11] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -913,7 +968,7 @@ func (x *GetServicesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetServicesResponse.ProtoReflect.Descriptor instead. func (*GetServicesResponse) Descriptor() ([]byte, []int) { - return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{11} + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{12} } func (x *GetServicesResponse) GetServices() []string { @@ -941,7 +996,7 @@ type DiffByReleasesRequest struct { func (x *DiffByReleasesRequest) Reset() { *x = DiffByReleasesRequest{} if protoimpl.UnsafeEnabled { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[12] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -954,7 +1009,7 @@ func (x *DiffByReleasesRequest) String() string { func (*DiffByReleasesRequest) ProtoMessage() {} func (x *DiffByReleasesRequest) ProtoReflect() protoreflect.Message { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[12] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -967,7 +1022,7 @@ func (x *DiffByReleasesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DiffByReleasesRequest.ProtoReflect.Descriptor instead. func (*DiffByReleasesRequest) Descriptor() ([]byte, []int) { - return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{12} + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{13} } func (x *DiffByReleasesRequest) GetService() string { @@ -1038,7 +1093,7 @@ type DiffByReleasesResponse struct { func (x *DiffByReleasesResponse) Reset() { *x = DiffByReleasesResponse{} if protoimpl.UnsafeEnabled { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[13] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1051,7 +1106,7 @@ func (x *DiffByReleasesResponse) String() string { func (*DiffByReleasesResponse) ProtoMessage() {} func (x *DiffByReleasesResponse) ProtoReflect() protoreflect.Message { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[13] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1064,7 +1119,7 @@ func (x *DiffByReleasesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DiffByReleasesResponse.ProtoReflect.Descriptor instead. func (*DiffByReleasesResponse) Descriptor() ([]byte, []int) { - return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{13} + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{14} } func (x *DiffByReleasesResponse) GetTotal() uint64 { @@ -1092,7 +1147,7 @@ type GetGroupsRequest_Filter struct { func (x *GetGroupsRequest_Filter) Reset() { *x = GetGroupsRequest_Filter{} if protoimpl.UnsafeEnabled { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[14] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1105,7 +1160,7 @@ func (x *GetGroupsRequest_Filter) String() string { func (*GetGroupsRequest_Filter) ProtoMessage() {} func (x *GetGroupsRequest_Filter) ProtoReflect() protoreflect.Message { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[14] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1128,6 +1183,164 @@ func (x *GetGroupsRequest_Filter) GetIsNew() bool { return false } +type GetGroupsResponse_Group struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Hash uint64 `protobuf:"varint,1,opt,name=hash,proto3" json:"hash,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + SeenTotal uint64 `protobuf:"varint,3,opt,name=seen_total,json=seenTotal,proto3" json:"seen_total,omitempty"` + FirstSeenAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=first_seen_at,json=firstSeenAt,proto3" json:"first_seen_at,omitempty"` + LastSeenAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=last_seen_at,json=lastSeenAt,proto3" json:"last_seen_at,omitempty"` + Source string `protobuf:"bytes,6,opt,name=source,proto3" json:"source,omitempty"` +} + +func (x *GetGroupsResponse_Group) Reset() { + *x = GetGroupsResponse_Group{} + if protoimpl.UnsafeEnabled { + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetGroupsResponse_Group) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetGroupsResponse_Group) ProtoMessage() {} + +func (x *GetGroupsResponse_Group) ProtoReflect() protoreflect.Message { + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[16] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetGroupsResponse_Group.ProtoReflect.Descriptor instead. +func (*GetGroupsResponse_Group) Descriptor() ([]byte, []int) { + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{1, 0} +} + +func (x *GetGroupsResponse_Group) GetHash() uint64 { + if x != nil { + return x.Hash + } + return 0 +} + +func (x *GetGroupsResponse_Group) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *GetGroupsResponse_Group) GetSeenTotal() uint64 { + if x != nil { + return x.SeenTotal + } + return 0 +} + +func (x *GetGroupsResponse_Group) GetFirstSeenAt() *timestamppb.Timestamp { + if x != nil { + return x.FirstSeenAt + } + return nil +} + +func (x *GetGroupsResponse_Group) GetLastSeenAt() *timestamppb.Timestamp { + if x != nil { + return x.LastSeenAt + } + return nil +} + +func (x *GetGroupsResponse_Group) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +type GetTopGroupsResponse_Group struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Hash uint64 `protobuf:"varint,1,opt,name=hash,proto3" json:"hash,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + Source string `protobuf:"bytes,3,opt,name=source,proto3" json:"source,omitempty"` + SeenTotal uint64 `protobuf:"varint,4,opt,name=seen_total,json=seenTotal,proto3" json:"seen_total,omitempty"` +} + +func (x *GetTopGroupsResponse_Group) Reset() { + *x = GetTopGroupsResponse_Group{} + if protoimpl.UnsafeEnabled { + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetTopGroupsResponse_Group) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTopGroupsResponse_Group) ProtoMessage() {} + +func (x *GetTopGroupsResponse_Group) ProtoReflect() protoreflect.Message { + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[17] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetTopGroupsResponse_Group.ProtoReflect.Descriptor instead. +func (*GetTopGroupsResponse_Group) Descriptor() ([]byte, []int) { + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{3, 0} +} + +func (x *GetTopGroupsResponse_Group) GetHash() uint64 { + if x != nil { + return x.Hash + } + return 0 +} + +func (x *GetTopGroupsResponse_Group) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *GetTopGroupsResponse_Group) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *GetTopGroupsResponse_Group) GetSeenTotal() uint64 { + if x != nil { + return x.SeenTotal + } + return 0 +} + type GetDetailsResponse_Distribution struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1140,7 +1353,7 @@ type GetDetailsResponse_Distribution struct { func (x *GetDetailsResponse_Distribution) Reset() { *x = GetDetailsResponse_Distribution{} if protoimpl.UnsafeEnabled { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[15] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1153,7 +1366,7 @@ func (x *GetDetailsResponse_Distribution) String() string { func (*GetDetailsResponse_Distribution) ProtoMessage() {} func (x *GetDetailsResponse_Distribution) ProtoReflect() protoreflect.Message { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[15] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1166,7 +1379,7 @@ func (x *GetDetailsResponse_Distribution) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDetailsResponse_Distribution.ProtoReflect.Descriptor instead. func (*GetDetailsResponse_Distribution) Descriptor() ([]byte, []int) { - return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{7, 0} + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{8, 0} } func (x *GetDetailsResponse_Distribution) GetValue() string { @@ -1190,12 +1403,14 @@ type GetDetailsResponse_Distributions struct { ByEnv []*GetDetailsResponse_Distribution `protobuf:"bytes,1,rep,name=by_env,json=byEnv,proto3" json:"by_env,omitempty"` ByRelease []*GetDetailsResponse_Distribution `protobuf:"bytes,2,rep,name=by_release,json=byRelease,proto3" json:"by_release,omitempty"` + BySource []*GetDetailsResponse_Distribution `protobuf:"bytes,3,rep,name=by_source,json=bySource,proto3" json:"by_source,omitempty"` + ByService []*GetDetailsResponse_Distribution `protobuf:"bytes,4,rep,name=by_service,json=byService,proto3" json:"by_service,omitempty"` } func (x *GetDetailsResponse_Distributions) Reset() { *x = GetDetailsResponse_Distributions{} if protoimpl.UnsafeEnabled { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[16] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1208,7 +1423,7 @@ func (x *GetDetailsResponse_Distributions) String() string { func (*GetDetailsResponse_Distributions) ProtoMessage() {} func (x *GetDetailsResponse_Distributions) ProtoReflect() protoreflect.Message { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[16] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1221,7 +1436,7 @@ func (x *GetDetailsResponse_Distributions) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDetailsResponse_Distributions.ProtoReflect.Descriptor instead. func (*GetDetailsResponse_Distributions) Descriptor() ([]byte, []int) { - return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{7, 1} + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{8, 1} } func (x *GetDetailsResponse_Distributions) GetByEnv() []*GetDetailsResponse_Distribution { @@ -1238,6 +1453,20 @@ func (x *GetDetailsResponse_Distributions) GetByRelease() []*GetDetailsResponse_ return nil } +func (x *GetDetailsResponse_Distributions) GetBySource() []*GetDetailsResponse_Distribution { + if x != nil { + return x.BySource + } + return nil +} + +func (x *GetDetailsResponse_Distributions) GetByService() []*GetDetailsResponse_Distribution { + if x != nil { + return x.ByService + } + return nil +} + type DiffByReleasesResponse_ReleaseInfo struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1249,7 +1478,7 @@ type DiffByReleasesResponse_ReleaseInfo struct { func (x *DiffByReleasesResponse_ReleaseInfo) Reset() { *x = DiffByReleasesResponse_ReleaseInfo{} if protoimpl.UnsafeEnabled { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[18] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1262,7 +1491,7 @@ func (x *DiffByReleasesResponse_ReleaseInfo) String() string { func (*DiffByReleasesResponse_ReleaseInfo) ProtoMessage() {} func (x *DiffByReleasesResponse_ReleaseInfo) ProtoReflect() protoreflect.Message { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[18] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1275,7 +1504,7 @@ func (x *DiffByReleasesResponse_ReleaseInfo) ProtoReflect() protoreflect.Message // Deprecated: Use DiffByReleasesResponse_ReleaseInfo.ProtoReflect.Descriptor instead. func (*DiffByReleasesResponse_ReleaseInfo) Descriptor() ([]byte, []int) { - return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{13, 0} + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{14, 0} } func (x *DiffByReleasesResponse_ReleaseInfo) GetSeenTotal() uint64 { @@ -1301,7 +1530,7 @@ type DiffByReleasesResponse_Group struct { func (x *DiffByReleasesResponse_Group) Reset() { *x = DiffByReleasesResponse_Group{} if protoimpl.UnsafeEnabled { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[19] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1314,7 +1543,7 @@ func (x *DiffByReleasesResponse_Group) String() string { func (*DiffByReleasesResponse_Group) ProtoMessage() {} func (x *DiffByReleasesResponse_Group) ProtoReflect() protoreflect.Message { - mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[19] + mi := &file_errorgroups_v1_errorgroups_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1327,7 +1556,7 @@ func (x *DiffByReleasesResponse_Group) ProtoReflect() protoreflect.Message { // Deprecated: Use DiffByReleasesResponse_Group.ProtoReflect.Descriptor instead. func (*DiffByReleasesResponse_Group) Descriptor() ([]byte, []int) { - return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{13, 1} + return file_errorgroups_v1_errorgroups_proto_rawDescGZIP(), []int{14, 1} } func (x *DiffByReleasesResponse_Group) GetHash() uint64 { @@ -1410,227 +1639,275 @@ var file_errorgroups_v1_errorgroups_proto_rawDesc = []byte{ 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x69, 0x73, 0x4e, 0x65, 0x77, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x65, 0x6e, 0x76, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x42, 0x09, 0x0a, 0x07, - 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x58, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x47, 0x72, - 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x74, 0x6f, 0x74, - 0x61, 0x6c, 0x12, 0x2d, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, - 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, - 0x73, 0x22, 0xea, 0x01, 0x0a, 0x05, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x68, - 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, - 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x65, - 0x6e, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, - 0x65, 0x65, 0x6e, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x3e, 0x0a, 0x0d, 0x66, 0x69, 0x72, 0x73, - 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x66, 0x69, 0x72, - 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, 0x3c, 0x0a, 0x0c, 0x6c, 0x61, 0x73, 0x74, - 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0xd7, 0x02, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x12, 0x3f, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x06, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x73, 0x1a, 0xea, 0x01, 0x0a, 0x05, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x12, + 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x68, 0x61, + 0x73, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1d, 0x0a, 0x0a, + 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x09, 0x73, 0x65, 0x65, 0x6e, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x3e, 0x0a, 0x0d, 0x66, + 0x69, 0x72, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, + 0x66, 0x69, 0x72, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, 0x3c, 0x0a, 0x0c, 0x6c, + 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x6c, + 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x22, 0xe0, 0x01, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x70, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x03, 0x65, 0x6e, 0x76, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x88, 0x01, 0x01, + 0x12, 0x1b, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x01, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, + 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, + 0x66, 0x73, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, + 0x65, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x77, 0x69, 0x74, 0x68, 0x54, 0x6f, 0x74, 0x61, + 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x65, 0x6e, 0x76, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x22, 0xde, 0x01, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x70, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x12, 0x42, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x70, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, + 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x1a, 0x6c, 0x0a, 0x05, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, + 0x68, 0x61, 0x73, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x16, + 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, 0x65, 0x65, 0x6e, + 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0xa9, 0x02, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x48, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x07, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x88, 0x01, 0x01, 0x12, 0x22, 0x0a, 0x0a, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x48, 0x01, 0x52, 0x09, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x48, 0x61, 0x73, 0x68, 0x88, 0x01, 0x01, 0x12, 0x15, 0x0a, 0x03, 0x65, + 0x6e, 0x76, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x88, + 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x88, 0x01, + 0x01, 0x12, 0x3a, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x04, + 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x1b, 0x0a, + 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x48, 0x05, 0x52, + 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x5f, 0x68, 0x61, 0x73, 0x68, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x65, 0x6e, 0x76, 0x42, 0x0a, 0x0a, + 0x08, 0x5f, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x64, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x22, 0x43, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x48, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x07, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, + 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x07, 0x62, + 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x22, 0x4e, 0x0a, 0x06, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, + 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, - 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x98, - 0x02, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x48, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x22, 0x0a, 0x0a, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x48, - 0x00, 0x52, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x48, 0x61, 0x73, 0x68, 0x88, 0x01, 0x01, 0x12, - 0x15, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x03, - 0x65, 0x6e, 0x76, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, - 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, - 0x73, 0x65, 0x88, 0x01, 0x01, 0x12, 0x3a, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x48, 0x03, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, - 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x48, 0x04, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0d, - 0x0a, 0x0b, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x42, 0x06, 0x0a, - 0x04, 0x5f, 0x65, 0x6e, 0x76, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, - 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x09, - 0x0a, 0x07, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x43, 0x0a, 0x0f, 0x47, 0x65, 0x74, - 0x48, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x07, - 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x42, - 0x75, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x07, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x22, 0x4e, - 0x0a, 0x06, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xbe, - 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1d, - 0x0a, 0x0a, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x48, 0x61, 0x73, 0x68, 0x12, 0x15, 0x0a, - 0x03, 0x65, 0x6e, 0x76, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x03, 0x65, 0x6e, - 0x76, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, - 0x88, 0x01, 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x88, 0x01, 0x01, - 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x65, 0x6e, 0x76, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x72, 0x65, 0x6c, - 0x65, 0x61, 0x73, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, - 0xcc, 0x05, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, - 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x48, 0x61, 0x73, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, - 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, 0x65, 0x65, 0x6e, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x3e, - 0x0a, 0x0d, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x52, 0x0b, 0x66, 0x69, 0x72, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, 0x3c, - 0x0a, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, 0x56, 0x0a, 0x0d, - 0x64, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, + 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xcf, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x44, 0x65, + 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x07, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, + 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x0a, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x48, 0x61, 0x73, 0x68, 0x12, 0x15, 0x0a, 0x03, 0x65, 0x6e, + 0x76, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x88, 0x01, + 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x02, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x88, 0x01, 0x01, + 0x12, 0x1b, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x03, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, + 0x08, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x65, 0x6e, + 0x76, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x42, 0x09, 0x0a, + 0x07, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0xea, 0x06, 0x0a, 0x12, 0x47, 0x65, 0x74, + 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x1d, 0x0a, 0x0a, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x48, 0x61, 0x73, 0x68, 0x12, 0x18, + 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x65, 0x6e, + 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, 0x65, + 0x65, 0x6e, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x3e, 0x0a, 0x0d, 0x66, 0x69, 0x72, 0x73, 0x74, + 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x66, 0x69, 0x72, 0x73, + 0x74, 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, 0x3c, 0x0a, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x5f, + 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x53, + 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, 0x56, 0x0a, 0x0d, 0x64, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, + 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x2e, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0d, + 0x64, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x4a, 0x0a, + 0x08, 0x6c, 0x6f, 0x67, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x2f, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x2e, 0x4c, 0x6f, 0x67, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x54, 0x61, 0x67, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x1a, 0x3e, 0x0a, 0x0c, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x65, 0x72, 0x63, 0x65, + 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, + 0x74, 0x1a, 0xc5, 0x02, 0x0a, 0x0d, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x46, 0x0a, 0x06, 0x62, 0x79, 0x5f, 0x65, 0x6e, 0x76, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x4a, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x5f, 0x74, 0x61, 0x67, 0x73, - 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, - 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x4c, 0x6f, 0x67, 0x54, 0x61, - 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x54, 0x61, 0x67, 0x73, - 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x1a, 0x3e, 0x0a, 0x0c, 0x44, 0x69, 0x73, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, - 0x0a, 0x07, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x07, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x1a, 0xa7, 0x01, 0x0a, 0x0d, 0x44, 0x69, 0x73, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x46, 0x0a, 0x06, 0x62, 0x79, - 0x5f, 0x65, 0x6e, 0x76, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, - 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x44, - 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x62, 0x79, 0x45, - 0x6e, 0x76, 0x12, 0x4e, 0x0a, 0x0a, 0x62, 0x79, 0x5f, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, - 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x44, 0x69, 0x73, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x62, 0x79, 0x52, 0x65, 0x6c, 0x65, 0x61, - 0x73, 0x65, 0x1a, 0x3a, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x53, - 0x0a, 0x12, 0x47, 0x65, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x15, - 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x03, 0x65, - 0x6e, 0x76, 0x88, 0x01, 0x01, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x65, 0x6e, 0x76, 0x4a, 0x04, 0x08, - 0x02, 0x10, 0x03, 0x22, 0x31, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, - 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, - 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, - 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x22, 0x77, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, - 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, - 0x72, 0x79, 0x12, 0x15, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, - 0x00, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x88, 0x01, 0x01, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, - 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, - 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x65, 0x6e, 0x76, 0x22, - 0x31, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x22, 0x8e, 0x02, 0x0a, 0x15, 0x44, 0x69, 0x66, 0x66, 0x42, 0x79, 0x52, 0x65, 0x6c, - 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, - 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, - 0x65, 0x73, 0x12, 0x15, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, - 0x00, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x88, 0x01, 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x06, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x88, 0x01, 0x01, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, - 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x6f, 0x66, - 0x66, 0x73, 0x65, 0x74, 0x12, 0x2b, 0x0a, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, - 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x05, 0x6f, 0x72, 0x64, 0x65, - 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x77, 0x69, 0x74, 0x68, 0x54, 0x6f, 0x74, 0x61, 0x6c, - 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x65, 0x6e, 0x76, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x22, 0xca, 0x04, 0x0a, 0x16, 0x44, 0x69, 0x66, 0x66, 0x42, 0x79, 0x52, 0x65, - 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x74, - 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x44, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x62, 0x79, 0x45, 0x6e, 0x76, 0x12, 0x4e, 0x0a, 0x0a, 0x62, + 0x79, 0x5f, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x2f, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x2e, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x09, 0x62, 0x79, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x09, 0x62, + 0x79, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, + 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x2e, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x08, 0x62, 0x79, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x4e, 0x0a, 0x0a, 0x62, 0x79, 0x5f, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, + 0x65, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x2e, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, + 0x62, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x1a, 0x3a, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, + 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x53, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x52, 0x65, 0x6c, 0x65, + 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x15, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x00, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x88, 0x01, 0x01, 0x42, 0x06, 0x0a, 0x04, + 0x5f, 0x65, 0x6e, 0x76, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x31, 0x0a, 0x13, 0x47, 0x65, + 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x22, 0x77, 0x0a, + 0x12, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x15, 0x0a, 0x03, 0x65, 0x6e, 0x76, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x88, 0x01, 0x01, + 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x42, 0x06, + 0x0a, 0x04, 0x5f, 0x65, 0x6e, 0x76, 0x22, 0x31, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, + 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x22, 0x8e, 0x02, 0x0a, 0x15, 0x44, 0x69, + 0x66, 0x66, 0x42, 0x79, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1a, 0x0a, + 0x08, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x08, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x12, 0x15, 0x0a, 0x03, 0x65, 0x6e, 0x76, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x88, 0x01, 0x01, + 0x12, 0x1b, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x01, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x88, 0x01, 0x01, 0x12, 0x14, 0x0a, + 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c, 0x69, + 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x2b, 0x0a, 0x05, 0x6f, + 0x72, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x72, 0x64, 0x65, + 0x72, 0x52, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x77, 0x69, 0x74, 0x68, + 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x77, 0x69, + 0x74, 0x68, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x65, 0x6e, 0x76, 0x42, + 0x09, 0x0a, 0x07, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0xca, 0x04, 0x0a, 0x16, 0x44, + 0x69, 0x66, 0x66, 0x42, 0x79, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x44, 0x0a, 0x06, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x66, + 0x66, 0x42, 0x79, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x73, 0x1a, 0x2c, 0x0a, 0x0b, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x66, 0x6f, + 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, 0x65, 0x65, 0x6e, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x1a, + 0xa5, 0x03, 0x0a, 0x05, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, + 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x18, 0x0a, + 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x3e, 0x0a, 0x0d, 0x66, 0x69, 0x72, 0x73, 0x74, + 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x66, 0x69, 0x72, 0x73, + 0x74, 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, 0x3c, 0x0a, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x5f, + 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x53, + 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x63, 0x0a, + 0x0d, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x73, 0x18, 0x06, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x66, 0x66, 0x42, 0x79, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x1a, 0x2c, 0x0a, 0x0b, 0x52, 0x65, - 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x65, - 0x6e, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, - 0x65, 0x65, 0x6e, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x1a, 0xa5, 0x03, 0x0a, 0x05, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x12, 0x3e, 0x0a, 0x0d, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, - 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x66, 0x69, 0x72, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, - 0x12, 0x3c, 0x0a, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, 0x16, - 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x63, 0x0a, 0x0d, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, - 0x65, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3e, 0x2e, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, - 0x69, 0x66, 0x66, 0x42, 0x79, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x2e, 0x52, 0x65, 0x6c, 0x65, - 0x61, 0x73, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x72, - 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x73, 0x1a, 0x73, 0x0a, 0x11, 0x52, - 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x48, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x32, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x44, 0x69, 0x66, 0x66, 0x42, 0x79, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, - 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x2a, 0x3f, 0x0a, 0x05, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x0e, 0x4f, 0x52, 0x44, - 0x45, 0x52, 0x5f, 0x46, 0x52, 0x45, 0x51, 0x55, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x10, 0x0a, - 0x0c, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x4c, 0x41, 0x54, 0x45, 0x53, 0x54, 0x10, 0x01, 0x12, - 0x10, 0x0a, 0x0c, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x4f, 0x4c, 0x44, 0x45, 0x53, 0x54, 0x10, - 0x02, 0x32, 0xa4, 0x04, 0x0a, 0x12, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, - 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x52, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x20, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, - 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, - 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4c, 0x0a, 0x07, - 0x47, 0x65, 0x74, 0x48, 0x69, 0x73, 0x74, 0x12, 0x1e, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x48, 0x69, 0x73, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x48, 0x69, 0x73, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x55, 0x0a, 0x0a, 0x47, 0x65, - 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x21, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x74, - 0x61, 0x69, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x65, 0x72, + 0x75, 0x70, 0x2e, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x66, + 0x6f, 0x73, 0x1a, 0x73, 0x0a, 0x11, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x66, + 0x6f, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x48, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x66, 0x66, 0x42, 0x79, + 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x2e, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x2a, 0x3f, 0x0a, 0x05, 0x4f, 0x72, 0x64, 0x65, 0x72, + 0x12, 0x12, 0x0a, 0x0e, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x46, 0x52, 0x45, 0x51, 0x55, 0x45, + 0x4e, 0x54, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x4c, 0x41, + 0x54, 0x45, 0x53, 0x54, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, + 0x4f, 0x4c, 0x44, 0x45, 0x53, 0x54, 0x10, 0x02, 0x32, 0x81, 0x05, 0x0a, 0x12, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, + 0x52, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x20, 0x2e, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, + 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, + 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x5b, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x70, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x73, 0x12, 0x23, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x70, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x70, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x4c, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x48, 0x69, 0x73, 0x74, 0x12, 0x1e, 0x2e, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, + 0x48, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, - 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x58, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, - 0x12, 0x22, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, + 0x48, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x55, + 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x21, 0x2e, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, + 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x22, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x52, 0x65, 0x6c, 0x65, + 0x61, 0x73, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x0b, 0x47, - 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, - 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x61, 0x0a, 0x0e, 0x44, 0x69, 0x66, 0x66, 0x42, 0x79, 0x52, - 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x12, 0x25, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x66, 0x66, 0x42, 0x79, 0x52, - 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x6c, + 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x58, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x44, 0x69, 0x66, 0x66, 0x42, 0x79, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x7a, 0x6f, 0x6e, 0x74, 0x65, 0x63, 0x68, 0x2f, - 0x73, 0x65, 0x71, 0x2d, 0x75, 0x69, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x61, 0x0a, 0x0e, 0x44, 0x69, 0x66, + 0x66, 0x42, 0x79, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x12, 0x25, 0x2e, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x66, + 0x66, 0x42, 0x79, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x66, 0x66, 0x42, 0x79, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, + 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x3b, 0x5a, 0x39, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x7a, 0x6f, 0x6e, 0x74, + 0x65, 0x63, 0x68, 0x2f, 0x73, 0x65, 0x71, 0x2d, 0x75, 0x69, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( @@ -1646,72 +1923,81 @@ func file_errorgroups_v1_errorgroups_proto_rawDescGZIP() []byte { } var file_errorgroups_v1_errorgroups_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_errorgroups_v1_errorgroups_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_errorgroups_v1_errorgroups_proto_msgTypes = make([]protoimpl.MessageInfo, 24) var file_errorgroups_v1_errorgroups_proto_goTypes = []any{ (Order)(0), // 0: errorgroups.v1.Order (*GetGroupsRequest)(nil), // 1: errorgroups.v1.GetGroupsRequest (*GetGroupsResponse)(nil), // 2: errorgroups.v1.GetGroupsResponse - (*Group)(nil), // 3: errorgroups.v1.Group - (*GetHistRequest)(nil), // 4: errorgroups.v1.GetHistRequest - (*GetHistResponse)(nil), // 5: errorgroups.v1.GetHistResponse - (*Bucket)(nil), // 6: errorgroups.v1.Bucket - (*GetDetailsRequest)(nil), // 7: errorgroups.v1.GetDetailsRequest - (*GetDetailsResponse)(nil), // 8: errorgroups.v1.GetDetailsResponse - (*GetReleasesRequest)(nil), // 9: errorgroups.v1.GetReleasesRequest - (*GetReleasesResponse)(nil), // 10: errorgroups.v1.GetReleasesResponse - (*GetServicesRequest)(nil), // 11: errorgroups.v1.GetServicesRequest - (*GetServicesResponse)(nil), // 12: errorgroups.v1.GetServicesResponse - (*DiffByReleasesRequest)(nil), // 13: errorgroups.v1.DiffByReleasesRequest - (*DiffByReleasesResponse)(nil), // 14: errorgroups.v1.DiffByReleasesResponse - (*GetGroupsRequest_Filter)(nil), // 15: errorgroups.v1.GetGroupsRequest.Filter - (*GetDetailsResponse_Distribution)(nil), // 16: errorgroups.v1.GetDetailsResponse.Distribution - (*GetDetailsResponse_Distributions)(nil), // 17: errorgroups.v1.GetDetailsResponse.Distributions - nil, // 18: errorgroups.v1.GetDetailsResponse.LogTagsEntry - (*DiffByReleasesResponse_ReleaseInfo)(nil), // 19: errorgroups.v1.DiffByReleasesResponse.ReleaseInfo - (*DiffByReleasesResponse_Group)(nil), // 20: errorgroups.v1.DiffByReleasesResponse.Group - nil, // 21: errorgroups.v1.DiffByReleasesResponse.Group.ReleaseInfosEntry - (*durationpb.Duration)(nil), // 22: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 23: google.protobuf.Timestamp + (*GetTopGroupsRequest)(nil), // 3: errorgroups.v1.GetTopGroupsRequest + (*GetTopGroupsResponse)(nil), // 4: errorgroups.v1.GetTopGroupsResponse + (*GetHistRequest)(nil), // 5: errorgroups.v1.GetHistRequest + (*GetHistResponse)(nil), // 6: errorgroups.v1.GetHistResponse + (*Bucket)(nil), // 7: errorgroups.v1.Bucket + (*GetDetailsRequest)(nil), // 8: errorgroups.v1.GetDetailsRequest + (*GetDetailsResponse)(nil), // 9: errorgroups.v1.GetDetailsResponse + (*GetReleasesRequest)(nil), // 10: errorgroups.v1.GetReleasesRequest + (*GetReleasesResponse)(nil), // 11: errorgroups.v1.GetReleasesResponse + (*GetServicesRequest)(nil), // 12: errorgroups.v1.GetServicesRequest + (*GetServicesResponse)(nil), // 13: errorgroups.v1.GetServicesResponse + (*DiffByReleasesRequest)(nil), // 14: errorgroups.v1.DiffByReleasesRequest + (*DiffByReleasesResponse)(nil), // 15: errorgroups.v1.DiffByReleasesResponse + (*GetGroupsRequest_Filter)(nil), // 16: errorgroups.v1.GetGroupsRequest.Filter + (*GetGroupsResponse_Group)(nil), // 17: errorgroups.v1.GetGroupsResponse.Group + (*GetTopGroupsResponse_Group)(nil), // 18: errorgroups.v1.GetTopGroupsResponse.Group + (*GetDetailsResponse_Distribution)(nil), // 19: errorgroups.v1.GetDetailsResponse.Distribution + (*GetDetailsResponse_Distributions)(nil), // 20: errorgroups.v1.GetDetailsResponse.Distributions + nil, // 21: errorgroups.v1.GetDetailsResponse.LogTagsEntry + (*DiffByReleasesResponse_ReleaseInfo)(nil), // 22: errorgroups.v1.DiffByReleasesResponse.ReleaseInfo + (*DiffByReleasesResponse_Group)(nil), // 23: errorgroups.v1.DiffByReleasesResponse.Group + nil, // 24: errorgroups.v1.DiffByReleasesResponse.Group.ReleaseInfosEntry + (*durationpb.Duration)(nil), // 25: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 26: google.protobuf.Timestamp } var file_errorgroups_v1_errorgroups_proto_depIdxs = []int32{ - 22, // 0: errorgroups.v1.GetGroupsRequest.duration:type_name -> google.protobuf.Duration + 25, // 0: errorgroups.v1.GetGroupsRequest.duration:type_name -> google.protobuf.Duration 0, // 1: errorgroups.v1.GetGroupsRequest.order:type_name -> errorgroups.v1.Order - 15, // 2: errorgroups.v1.GetGroupsRequest.filter:type_name -> errorgroups.v1.GetGroupsRequest.Filter - 3, // 3: errorgroups.v1.GetGroupsResponse.groups:type_name -> errorgroups.v1.Group - 23, // 4: errorgroups.v1.Group.first_seen_at:type_name -> google.protobuf.Timestamp - 23, // 5: errorgroups.v1.Group.last_seen_at:type_name -> google.protobuf.Timestamp - 22, // 6: errorgroups.v1.GetHistRequest.duration:type_name -> google.protobuf.Duration - 6, // 7: errorgroups.v1.GetHistResponse.buckets:type_name -> errorgroups.v1.Bucket - 23, // 8: errorgroups.v1.Bucket.time:type_name -> google.protobuf.Timestamp - 23, // 9: errorgroups.v1.GetDetailsResponse.first_seen_at:type_name -> google.protobuf.Timestamp - 23, // 10: errorgroups.v1.GetDetailsResponse.last_seen_at:type_name -> google.protobuf.Timestamp - 17, // 11: errorgroups.v1.GetDetailsResponse.distributions:type_name -> errorgroups.v1.GetDetailsResponse.Distributions - 18, // 12: errorgroups.v1.GetDetailsResponse.log_tags:type_name -> errorgroups.v1.GetDetailsResponse.LogTagsEntry + 16, // 2: errorgroups.v1.GetGroupsRequest.filter:type_name -> errorgroups.v1.GetGroupsRequest.Filter + 17, // 3: errorgroups.v1.GetGroupsResponse.groups:type_name -> errorgroups.v1.GetGroupsResponse.Group + 25, // 4: errorgroups.v1.GetTopGroupsRequest.duration:type_name -> google.protobuf.Duration + 18, // 5: errorgroups.v1.GetTopGroupsResponse.groups:type_name -> errorgroups.v1.GetTopGroupsResponse.Group + 25, // 6: errorgroups.v1.GetHistRequest.duration:type_name -> google.protobuf.Duration + 7, // 7: errorgroups.v1.GetHistResponse.buckets:type_name -> errorgroups.v1.Bucket + 26, // 8: errorgroups.v1.Bucket.time:type_name -> google.protobuf.Timestamp + 26, // 9: errorgroups.v1.GetDetailsResponse.first_seen_at:type_name -> google.protobuf.Timestamp + 26, // 10: errorgroups.v1.GetDetailsResponse.last_seen_at:type_name -> google.protobuf.Timestamp + 20, // 11: errorgroups.v1.GetDetailsResponse.distributions:type_name -> errorgroups.v1.GetDetailsResponse.Distributions + 21, // 12: errorgroups.v1.GetDetailsResponse.log_tags:type_name -> errorgroups.v1.GetDetailsResponse.LogTagsEntry 0, // 13: errorgroups.v1.DiffByReleasesRequest.order:type_name -> errorgroups.v1.Order - 20, // 14: errorgroups.v1.DiffByReleasesResponse.groups:type_name -> errorgroups.v1.DiffByReleasesResponse.Group - 16, // 15: errorgroups.v1.GetDetailsResponse.Distributions.by_env:type_name -> errorgroups.v1.GetDetailsResponse.Distribution - 16, // 16: errorgroups.v1.GetDetailsResponse.Distributions.by_release:type_name -> errorgroups.v1.GetDetailsResponse.Distribution - 23, // 17: errorgroups.v1.DiffByReleasesResponse.Group.first_seen_at:type_name -> google.protobuf.Timestamp - 23, // 18: errorgroups.v1.DiffByReleasesResponse.Group.last_seen_at:type_name -> google.protobuf.Timestamp - 21, // 19: errorgroups.v1.DiffByReleasesResponse.Group.release_infos:type_name -> errorgroups.v1.DiffByReleasesResponse.Group.ReleaseInfosEntry - 19, // 20: errorgroups.v1.DiffByReleasesResponse.Group.ReleaseInfosEntry.value:type_name -> errorgroups.v1.DiffByReleasesResponse.ReleaseInfo - 1, // 21: errorgroups.v1.ErrorGroupsService.GetGroups:input_type -> errorgroups.v1.GetGroupsRequest - 4, // 22: errorgroups.v1.ErrorGroupsService.GetHist:input_type -> errorgroups.v1.GetHistRequest - 7, // 23: errorgroups.v1.ErrorGroupsService.GetDetails:input_type -> errorgroups.v1.GetDetailsRequest - 9, // 24: errorgroups.v1.ErrorGroupsService.GetReleases:input_type -> errorgroups.v1.GetReleasesRequest - 11, // 25: errorgroups.v1.ErrorGroupsService.GetServices:input_type -> errorgroups.v1.GetServicesRequest - 13, // 26: errorgroups.v1.ErrorGroupsService.DiffByReleases:input_type -> errorgroups.v1.DiffByReleasesRequest - 2, // 27: errorgroups.v1.ErrorGroupsService.GetGroups:output_type -> errorgroups.v1.GetGroupsResponse - 5, // 28: errorgroups.v1.ErrorGroupsService.GetHist:output_type -> errorgroups.v1.GetHistResponse - 8, // 29: errorgroups.v1.ErrorGroupsService.GetDetails:output_type -> errorgroups.v1.GetDetailsResponse - 10, // 30: errorgroups.v1.ErrorGroupsService.GetReleases:output_type -> errorgroups.v1.GetReleasesResponse - 12, // 31: errorgroups.v1.ErrorGroupsService.GetServices:output_type -> errorgroups.v1.GetServicesResponse - 14, // 32: errorgroups.v1.ErrorGroupsService.DiffByReleases:output_type -> errorgroups.v1.DiffByReleasesResponse - 27, // [27:33] is the sub-list for method output_type - 21, // [21:27] is the sub-list for method input_type - 21, // [21:21] is the sub-list for extension type_name - 21, // [21:21] is the sub-list for extension extendee - 0, // [0:21] is the sub-list for field type_name + 23, // 14: errorgroups.v1.DiffByReleasesResponse.groups:type_name -> errorgroups.v1.DiffByReleasesResponse.Group + 26, // 15: errorgroups.v1.GetGroupsResponse.Group.first_seen_at:type_name -> google.protobuf.Timestamp + 26, // 16: errorgroups.v1.GetGroupsResponse.Group.last_seen_at:type_name -> google.protobuf.Timestamp + 19, // 17: errorgroups.v1.GetDetailsResponse.Distributions.by_env:type_name -> errorgroups.v1.GetDetailsResponse.Distribution + 19, // 18: errorgroups.v1.GetDetailsResponse.Distributions.by_release:type_name -> errorgroups.v1.GetDetailsResponse.Distribution + 19, // 19: errorgroups.v1.GetDetailsResponse.Distributions.by_source:type_name -> errorgroups.v1.GetDetailsResponse.Distribution + 19, // 20: errorgroups.v1.GetDetailsResponse.Distributions.by_service:type_name -> errorgroups.v1.GetDetailsResponse.Distribution + 26, // 21: errorgroups.v1.DiffByReleasesResponse.Group.first_seen_at:type_name -> google.protobuf.Timestamp + 26, // 22: errorgroups.v1.DiffByReleasesResponse.Group.last_seen_at:type_name -> google.protobuf.Timestamp + 24, // 23: errorgroups.v1.DiffByReleasesResponse.Group.release_infos:type_name -> errorgroups.v1.DiffByReleasesResponse.Group.ReleaseInfosEntry + 22, // 24: errorgroups.v1.DiffByReleasesResponse.Group.ReleaseInfosEntry.value:type_name -> errorgroups.v1.DiffByReleasesResponse.ReleaseInfo + 1, // 25: errorgroups.v1.ErrorGroupsService.GetGroups:input_type -> errorgroups.v1.GetGroupsRequest + 3, // 26: errorgroups.v1.ErrorGroupsService.GetTopGroups:input_type -> errorgroups.v1.GetTopGroupsRequest + 5, // 27: errorgroups.v1.ErrorGroupsService.GetHist:input_type -> errorgroups.v1.GetHistRequest + 8, // 28: errorgroups.v1.ErrorGroupsService.GetDetails:input_type -> errorgroups.v1.GetDetailsRequest + 10, // 29: errorgroups.v1.ErrorGroupsService.GetReleases:input_type -> errorgroups.v1.GetReleasesRequest + 12, // 30: errorgroups.v1.ErrorGroupsService.GetServices:input_type -> errorgroups.v1.GetServicesRequest + 14, // 31: errorgroups.v1.ErrorGroupsService.DiffByReleases:input_type -> errorgroups.v1.DiffByReleasesRequest + 2, // 32: errorgroups.v1.ErrorGroupsService.GetGroups:output_type -> errorgroups.v1.GetGroupsResponse + 4, // 33: errorgroups.v1.ErrorGroupsService.GetTopGroups:output_type -> errorgroups.v1.GetTopGroupsResponse + 6, // 34: errorgroups.v1.ErrorGroupsService.GetHist:output_type -> errorgroups.v1.GetHistResponse + 9, // 35: errorgroups.v1.ErrorGroupsService.GetDetails:output_type -> errorgroups.v1.GetDetailsResponse + 11, // 36: errorgroups.v1.ErrorGroupsService.GetReleases:output_type -> errorgroups.v1.GetReleasesResponse + 13, // 37: errorgroups.v1.ErrorGroupsService.GetServices:output_type -> errorgroups.v1.GetServicesResponse + 15, // 38: errorgroups.v1.ErrorGroupsService.DiffByReleases:output_type -> errorgroups.v1.DiffByReleasesResponse + 32, // [32:39] is the sub-list for method output_type + 25, // [25:32] is the sub-list for method input_type + 25, // [25:25] is the sub-list for extension type_name + 25, // [25:25] is the sub-list for extension extendee + 0, // [0:25] is the sub-list for field type_name } func init() { file_errorgroups_v1_errorgroups_proto_init() } @@ -1745,7 +2031,7 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[2].Exporter = func(v any, i int) any { - switch v := v.(*Group); i { + switch v := v.(*GetTopGroupsRequest); i { case 0: return &v.state case 1: @@ -1757,7 +2043,7 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[3].Exporter = func(v any, i int) any { - switch v := v.(*GetHistRequest); i { + switch v := v.(*GetTopGroupsResponse); i { case 0: return &v.state case 1: @@ -1769,7 +2055,7 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[4].Exporter = func(v any, i int) any { - switch v := v.(*GetHistResponse); i { + switch v := v.(*GetHistRequest); i { case 0: return &v.state case 1: @@ -1781,7 +2067,7 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[5].Exporter = func(v any, i int) any { - switch v := v.(*Bucket); i { + switch v := v.(*GetHistResponse); i { case 0: return &v.state case 1: @@ -1793,7 +2079,7 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[6].Exporter = func(v any, i int) any { - switch v := v.(*GetDetailsRequest); i { + switch v := v.(*Bucket); i { case 0: return &v.state case 1: @@ -1805,7 +2091,7 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[7].Exporter = func(v any, i int) any { - switch v := v.(*GetDetailsResponse); i { + switch v := v.(*GetDetailsRequest); i { case 0: return &v.state case 1: @@ -1817,7 +2103,7 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[8].Exporter = func(v any, i int) any { - switch v := v.(*GetReleasesRequest); i { + switch v := v.(*GetDetailsResponse); i { case 0: return &v.state case 1: @@ -1829,7 +2115,7 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[9].Exporter = func(v any, i int) any { - switch v := v.(*GetReleasesResponse); i { + switch v := v.(*GetReleasesRequest); i { case 0: return &v.state case 1: @@ -1841,7 +2127,7 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[10].Exporter = func(v any, i int) any { - switch v := v.(*GetServicesRequest); i { + switch v := v.(*GetReleasesResponse); i { case 0: return &v.state case 1: @@ -1853,7 +2139,7 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[11].Exporter = func(v any, i int) any { - switch v := v.(*GetServicesResponse); i { + switch v := v.(*GetServicesRequest); i { case 0: return &v.state case 1: @@ -1865,7 +2151,7 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[12].Exporter = func(v any, i int) any { - switch v := v.(*DiffByReleasesRequest); i { + switch v := v.(*GetServicesResponse); i { case 0: return &v.state case 1: @@ -1877,7 +2163,7 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[13].Exporter = func(v any, i int) any { - switch v := v.(*DiffByReleasesResponse); i { + switch v := v.(*DiffByReleasesRequest); i { case 0: return &v.state case 1: @@ -1889,7 +2175,7 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[14].Exporter = func(v any, i int) any { - switch v := v.(*GetGroupsRequest_Filter); i { + switch v := v.(*DiffByReleasesResponse); i { case 0: return &v.state case 1: @@ -1901,7 +2187,7 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[15].Exporter = func(v any, i int) any { - switch v := v.(*GetDetailsResponse_Distribution); i { + switch v := v.(*GetGroupsRequest_Filter); i { case 0: return &v.state case 1: @@ -1913,7 +2199,19 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[16].Exporter = func(v any, i int) any { - switch v := v.(*GetDetailsResponse_Distributions); i { + switch v := v.(*GetGroupsResponse_Group); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_errorgroups_v1_errorgroups_proto_msgTypes[17].Exporter = func(v any, i int) any { + switch v := v.(*GetTopGroupsResponse_Group); i { case 0: return &v.state case 1: @@ -1925,7 +2223,7 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[18].Exporter = func(v any, i int) any { - switch v := v.(*DiffByReleasesResponse_ReleaseInfo); i { + switch v := v.(*GetDetailsResponse_Distribution); i { case 0: return &v.state case 1: @@ -1937,6 +2235,30 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[19].Exporter = func(v any, i int) any { + switch v := v.(*GetDetailsResponse_Distributions); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_errorgroups_v1_errorgroups_proto_msgTypes[21].Exporter = func(v any, i int) any { + switch v := v.(*DiffByReleasesResponse_ReleaseInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_errorgroups_v1_errorgroups_proto_msgTypes[22].Exporter = func(v any, i int) any { switch v := v.(*DiffByReleasesResponse_Group); i { case 0: return &v.state @@ -1950,18 +2272,19 @@ func file_errorgroups_v1_errorgroups_proto_init() { } } file_errorgroups_v1_errorgroups_proto_msgTypes[0].OneofWrappers = []any{} - file_errorgroups_v1_errorgroups_proto_msgTypes[3].OneofWrappers = []any{} - file_errorgroups_v1_errorgroups_proto_msgTypes[6].OneofWrappers = []any{} - file_errorgroups_v1_errorgroups_proto_msgTypes[8].OneofWrappers = []any{} - file_errorgroups_v1_errorgroups_proto_msgTypes[10].OneofWrappers = []any{} - file_errorgroups_v1_errorgroups_proto_msgTypes[12].OneofWrappers = []any{} + file_errorgroups_v1_errorgroups_proto_msgTypes[2].OneofWrappers = []any{} + file_errorgroups_v1_errorgroups_proto_msgTypes[4].OneofWrappers = []any{} + file_errorgroups_v1_errorgroups_proto_msgTypes[7].OneofWrappers = []any{} + file_errorgroups_v1_errorgroups_proto_msgTypes[9].OneofWrappers = []any{} + file_errorgroups_v1_errorgroups_proto_msgTypes[11].OneofWrappers = []any{} + file_errorgroups_v1_errorgroups_proto_msgTypes[13].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_errorgroups_v1_errorgroups_proto_rawDesc, NumEnums: 1, - NumMessages: 21, + NumMessages: 24, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/errorgroups/v1/errorgroups_grpc.pb.go b/pkg/errorgroups/v1/errorgroups_grpc.pb.go index ba752fa..da1df31 100644 --- a/pkg/errorgroups/v1/errorgroups_grpc.pb.go +++ b/pkg/errorgroups/v1/errorgroups_grpc.pb.go @@ -20,6 +20,7 @@ const _ = grpc.SupportPackageIsVersion8 const ( ErrorGroupsService_GetGroups_FullMethodName = "/errorgroups.v1.ErrorGroupsService/GetGroups" + ErrorGroupsService_GetTopGroups_FullMethodName = "/errorgroups.v1.ErrorGroupsService/GetTopGroups" ErrorGroupsService_GetHist_FullMethodName = "/errorgroups.v1.ErrorGroupsService/GetHist" ErrorGroupsService_GetDetails_FullMethodName = "/errorgroups.v1.ErrorGroupsService/GetDetails" ErrorGroupsService_GetReleases_FullMethodName = "/errorgroups.v1.ErrorGroupsService/GetReleases" @@ -32,6 +33,7 @@ const ( // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type ErrorGroupsServiceClient interface { GetGroups(ctx context.Context, in *GetGroupsRequest, opts ...grpc.CallOption) (*GetGroupsResponse, error) + GetTopGroups(ctx context.Context, in *GetTopGroupsRequest, opts ...grpc.CallOption) (*GetTopGroupsResponse, error) GetHist(ctx context.Context, in *GetHistRequest, opts ...grpc.CallOption) (*GetHistResponse, error) GetDetails(ctx context.Context, in *GetDetailsRequest, opts ...grpc.CallOption) (*GetDetailsResponse, error) GetReleases(ctx context.Context, in *GetReleasesRequest, opts ...grpc.CallOption) (*GetReleasesResponse, error) @@ -57,6 +59,16 @@ func (c *errorGroupsServiceClient) GetGroups(ctx context.Context, in *GetGroupsR return out, nil } +func (c *errorGroupsServiceClient) GetTopGroups(ctx context.Context, in *GetTopGroupsRequest, opts ...grpc.CallOption) (*GetTopGroupsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetTopGroupsResponse) + err := c.cc.Invoke(ctx, ErrorGroupsService_GetTopGroups_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *errorGroupsServiceClient) GetHist(ctx context.Context, in *GetHistRequest, opts ...grpc.CallOption) (*GetHistResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetHistResponse) @@ -112,6 +124,7 @@ func (c *errorGroupsServiceClient) DiffByReleases(ctx context.Context, in *DiffB // for forward compatibility type ErrorGroupsServiceServer interface { GetGroups(context.Context, *GetGroupsRequest) (*GetGroupsResponse, error) + GetTopGroups(context.Context, *GetTopGroupsRequest) (*GetTopGroupsResponse, error) GetHist(context.Context, *GetHistRequest) (*GetHistResponse, error) GetDetails(context.Context, *GetDetailsRequest) (*GetDetailsResponse, error) GetReleases(context.Context, *GetReleasesRequest) (*GetReleasesResponse, error) @@ -126,6 +139,9 @@ type UnimplementedErrorGroupsServiceServer struct { func (UnimplementedErrorGroupsServiceServer) GetGroups(context.Context, *GetGroupsRequest) (*GetGroupsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetGroups not implemented") } +func (UnimplementedErrorGroupsServiceServer) GetTopGroups(context.Context, *GetTopGroupsRequest) (*GetTopGroupsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetTopGroups not implemented") +} func (UnimplementedErrorGroupsServiceServer) GetHist(context.Context, *GetHistRequest) (*GetHistResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetHist not implemented") } @@ -171,6 +187,24 @@ func _ErrorGroupsService_GetGroups_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _ErrorGroupsService_GetTopGroups_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetTopGroupsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ErrorGroupsServiceServer).GetTopGroups(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ErrorGroupsService_GetTopGroups_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ErrorGroupsServiceServer).GetTopGroups(ctx, req.(*GetTopGroupsRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _ErrorGroupsService_GetHist_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetHistRequest) if err := dec(in); err != nil { @@ -272,6 +306,10 @@ var ErrorGroupsService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetGroups", Handler: _ErrorGroupsService_GetGroups_Handler, }, + { + MethodName: "GetTopGroups", + Handler: _ErrorGroupsService_GetTopGroups_Handler, + }, { MethodName: "GetHist", Handler: _ErrorGroupsService_GetHist_Handler, diff --git a/swagger/swagger.json b/swagger/swagger.json index 8a0e31c..4f3792f 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -499,6 +499,44 @@ } } }, + "/errorgroups/v1/top_groups": { + "post": { + "security": [ + { + "bearer": [] + } + ], + "tags": [ + "errorgroups_v1" + ], + "operationId": "errorgroups_v1_get_top_groups", + "parameters": [ + { + "description": "Request body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/errorgroups.v1.GetTopGroupsRequest" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/errorgroups.v1.GetTopGroupsResponse" + } + }, + "default": { + "description": "An unexpected error response", + "schema": { + "$ref": "#/definitions/UnexpectedError" + } + } + } + } + }, "/massexport/v1/cancel": { "post": { "security": [ @@ -1737,17 +1775,18 @@ "items": { "$ref": "#/definitions/errorgroups.v1.Distribution" } - } - } - }, - "errorgroups.v1.Env": { - "type": "object", - "properties": { - "env": { - "type": "string" }, - "percent": { - "type": "integer" + "by_service": { + "type": "array", + "items": { + "$ref": "#/definitions/errorgroups.v1.Distribution" + } + }, + "by_source": { + "type": "array", + "items": { + "$ref": "#/definitions/errorgroups.v1.Distribution" + } } } }, @@ -1778,13 +1817,6 @@ "distributions": { "$ref": "#/definitions/errorgroups.v1.Distributions" }, - "envs": { - "description": "Deprecated. Use `distributions.by_envs` instead", - "type": "array", - "items": { - "$ref": "#/definitions/errorgroups.v1.Env" - } - }, "first_seen_at": { "type": "string", "format": "date-time" @@ -1954,6 +1986,46 @@ } } }, + "errorgroups.v1.GetTopGroupsRequest": { + "type": "object", + "properties": { + "duration": { + "description": "In go duration format. If not specified, then for the entire time.", + "type": "string", + "format": "duration", + "example": "1h" + }, + "env": { + "type": "string" + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "source": { + "type": "string" + }, + "with_total": { + "type": "boolean" + } + } + }, + "errorgroups.v1.GetTopGroupsResponse": { + "type": "object", + "properties": { + "groups": { + "type": "array", + "items": { + "$ref": "#/definitions/errorgroups.v1.TopGroup" + } + }, + "total": { + "type": "integer" + } + } + }, "errorgroups.v1.Group": { "type": "object", "properties": { @@ -2001,6 +2073,24 @@ "OrderOldest" ] }, + "errorgroups.v1.TopGroup": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "format": "uint64" + }, + "message": { + "type": "string" + }, + "seen_total": { + "type": "integer" + }, + "source": { + "type": "string" + } + } + }, "http.AsyncSearchRequestHistogram": { "type": "object", "properties": {