From 8340232b0435baac698b637add121f38eaddf951 Mon Sep 17 00:00:00 2001 From: Edward Welch Date: Fri, 10 May 2019 13:54:09 -0400 Subject: [PATCH 01/17] initial checkin mostly working, need to extract out the config needs a proper ksonnet or helm deployment or both needs a makefile --- .gitignore | 0 Dockerfile | 9 + README.md | 5 + cmd/loki-canary/main.go | 55 + go.mod | 17 + go.sum | 113 ++ pkg/comparator/comparator.go | 137 ++ pkg/comparator/comparator_test.go | 89 + pkg/reader/logproto.pb.go | 2569 +++++++++++++++++++++++++++++ pkg/reader/reader.go | 113 ++ pkg/writer/writer.go | 81 + 11 files changed, 3188 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 cmd/loki-canary/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/comparator/comparator.go create mode 100644 pkg/comparator/comparator_test.go create mode 100644 pkg/reader/logproto.pb.go create mode 100644 pkg/reader/reader.go create mode 100644 pkg/writer/writer.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000000..068429b86c57 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM golang +COPY ./ /src/ +RUN cd /src && \ + CGO_ENABLED=0 go build -o loki-canary cmd/loki-canary/main.go + +FROM alpine:3.9 +RUN apk add --update --no-cache ca-certificates +COPY --from=0 /src/loki-canary /bin/loki-canary +ENTRYPOINT [ "/bin/loki-canary" ] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000000..b24e09dae283 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ + + +`docker build -t loki-canary:latest .` + +`kubectl run loki-canary --generator=run-pod/v1 --image=loki-canary:latest --restart=Never --image-pull-policy=Never --labels=name=loki-canary` \ No newline at end of file diff --git a/cmd/loki-canary/main.go b/cmd/loki-canary/main.go new file mode 100644 index 000000000000..40dab0d4701e --- /dev/null +++ b/cmd/loki-canary/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + "net/http" + "net/url" + "os" + "os/signal" + "time" + + "github.com/prometheus/client_golang/prometheus/promhttp" + + "github.com/grafana/loki-canary/pkg/comparator" + "github.com/grafana/loki-canary/pkg/reader" + "github.com/grafana/loki-canary/pkg/writer" +) + +func main() { + + c := comparator.NewComparator(os.Stderr, 1*time.Minute, 1*time.Second) + + w := writer.NewWriter(os.Stdout, c, 10*time.Millisecond, 1024) + + u := url.URL{ + Scheme: "ws", + Host: "loki:3100", + Path: "/api/prom/tail", + RawQuery: "query=" + url.QueryEscape("{name=\"loki-canary\",stream=\"stdout\"}"), + } + + r := reader.NewReader(os.Stderr, c, u, "", "") + + http.Handle("/metrics", promhttp.Handler()) + go func() { + err := http.ListenAndServe(":2112", nil) + if err != nil { + panic(err) + } + }() + + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + + for { + select { + case <-interrupt: + _, _ = fmt.Fprintf(os.Stderr, "shutting down\n") + w.Stop() + r.Stop() + c.Stop() + return + } + } + +} diff --git a/go.mod b/go.mod new file mode 100644 index 000000000000..4703d635d1b0 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/grafana/loki-canary + +go 1.12 + +require ( + github.com/gogo/protobuf v1.2.1 + github.com/golang/protobuf v1.3.1 // indirect + github.com/gorilla/websocket v1.4.0 + github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 + github.com/prometheus/common v0.3.0 // indirect + github.com/stretchr/testify v1.3.0 + golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 // indirect + golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 // indirect + google.golang.org/appengine v1.4.0 // indirect + google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 // indirect + google.golang.org/grpc v1.20.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000000..bad7dc6e3465 --- /dev/null +++ b/go.sum @@ -0,0 +1,113 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 h1:D+CiwcpGTW6pL6bv6KI3KbyEyCKyS+1JWS2h8PNDnGA= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f h1:BVwpUVJDADN2ufcGik7W992pyps0wZ888b/y9GXcLTU= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.3.0 h1:taZ4h8Tkxv2kNyoSctBvfXEHmBmxrwmIidZTIaHons4= +github.com/prometheus/common v0.3.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1 h1:/K3IL0Z1quvmJ7X0A1AwNEK7CRkVK3YwfOU/QAL4WGg= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +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= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/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= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/comparator/comparator.go b/pkg/comparator/comparator.go new file mode 100644 index 000000000000..7038acaee586 --- /dev/null +++ b/pkg/comparator/comparator.go @@ -0,0 +1,137 @@ +package comparator + +import ( + "fmt" + "io" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +const ( + ErrOutOfOrderEntry = "entry %s was received before entries: %v\n" + ErrEntryNotReceived = "failed to receive entry %s within %f seconds\n" +) + +var ( + outOfOrderEntry = promauto.NewCounter(prometheus.CounterOpts{ + Name: "out_of_order_entry", + Help: "The total number of processed events", + }) + missingEntry = promauto.NewCounter(prometheus.CounterOpts{ + Name: "missing_entry", + Help: "The total number of processed events", + }) +) + +type Comparator struct { + entMtx sync.Mutex + w io.Writer + entries []*time.Time + maxWait time.Duration + pruneInterval time.Duration + quit chan struct{} + done chan struct{} +} + +func NewComparator(writer io.Writer, maxWait time.Duration, pruneInterval time.Duration) *Comparator { + c := &Comparator{ + w: writer, + entries: []*time.Time{}, + maxWait: maxWait, + pruneInterval: pruneInterval, + quit: make(chan struct{}), + done: make(chan struct{}), + } + + go c.run() + + return c +} + +func (c *Comparator) Stop() { + close(c.quit) + <-c.done +} + +func (c *Comparator) EntrySent(time time.Time) { + c.entMtx.Lock() + defer c.entMtx.Unlock() + c.entries = append(c.entries, &time) +} + +func (c *Comparator) EntryReceived(ts time.Time) { + c.entMtx.Lock() + defer c.entMtx.Unlock() + + // Output index + k := 0 + for i, e := range c.entries { + if ts.Equal(*e) { + // If this isn't the first item in the list we received it out of order + if i != 0 { + outOfOrderEntry.Inc() + _, _ = fmt.Fprintf(c.w, ErrOutOfOrderEntry, e, c.entries[:i]) + } + // Do not increment output index, effectively causing this element to be dropped + } else { + // If the current index doesn't match the output index, update the array with the correct position + if i != k { + c.entries[k] = c.entries[i] + } + k++ + } + } + // Nil out the pointers to any trailing elements which were removed from the slice + for i := k; i < len(c.entries); i++ { + c.entries[i] = nil // or the zero value of T + } + c.entries = c.entries[:k] +} + +func (c *Comparator) Size() int { + return len(c.entries) +} + +func (c *Comparator) run() { + t := time.NewTicker(c.pruneInterval) + defer func() { + t.Stop() + close(c.done) + }() + + for { + select { + case <-t.C: + c.pruneEntries() + case <-c.quit: + return + } + } +} + +func (c *Comparator) pruneEntries() { + c.entMtx.Lock() + defer c.entMtx.Unlock() + + k := 0 + for i, e := range c.entries { + // If the time is outside our range, assume the entry has been lost report and remove it + if e.Before(time.Now().Add(-c.maxWait)) { + missingEntry.Inc() + _, _ = fmt.Fprintf(c.w, ErrEntryNotReceived, e, c.maxWait.Seconds()) + } else { + if i != k { + c.entries[k] = c.entries[i] + } + k++ + } + } + // Nil out the pointers to any trailing elements which were removed from the slice + for i := k; i < len(c.entries); i++ { + c.entries[i] = nil // or the zero value of T + } + c.entries = c.entries[:k] +} diff --git a/pkg/comparator/comparator_test.go b/pkg/comparator/comparator_test.go new file mode 100644 index 000000000000..37275e8aba50 --- /dev/null +++ b/pkg/comparator/comparator_test.go @@ -0,0 +1,89 @@ +package comparator + +import ( + "bytes" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestComparatorEntryReceivedOutOfOrder(t *testing.T) { + actual := &bytes.Buffer{} + c := NewComparator(actual, 1*time.Hour, 1*time.Hour) + + t1 := time.Now() + t2 := t1.Add(1 * time.Second) + t3 := t2.Add(1 * time.Second) + t4 := t3.Add(1 * time.Second) + + c.EntrySent(t1) + c.EntrySent(t2) + c.EntrySent(t3) + c.EntrySent(t4) + + c.EntryReceived(t1) + c.EntryReceived(t4) + expected := fmt.Sprintf(ErrOutOfOrderEntry, t4, []time.Time{t2, t3}) + + assert.Equal(t, expected, actual.String()) +} + +func TestComparatorEntryReceivedNotExpected(t *testing.T) { + actual := &bytes.Buffer{} + c := NewComparator(actual, 1*time.Hour, 1*time.Hour) + + t1 := time.Now() + t2 := t1.Add(1 * time.Second) + t3 := t2.Add(1 * time.Second) + t4 := t3.Add(1 * time.Second) + + c.EntrySent(t2) + c.EntrySent(t3) + c.EntrySent(t4) + + c.EntryReceived(t2) + assert.Equal(t, 2, c.Size()) + c.EntryReceived(t1) + assert.Equal(t, 2, c.Size()) + c.EntryReceived(t3) + assert.Equal(t, 1, c.Size()) + c.EntryReceived(t4) + assert.Equal(t, 0, c.Size()) + expected := "" + + assert.Equal(t, expected, actual.String()) +} + +func TestEntryNeverReceived(t *testing.T) { + + actual := &bytes.Buffer{} + c := NewComparator(actual, 5*time.Millisecond, 2*time.Millisecond) + + t1 := time.Now() + t2 := t1.Add(1 * time.Millisecond) + t3 := t2.Add(1 * time.Millisecond) + t4 := t3.Add(1 * time.Millisecond) + + c.EntrySent(t1) + c.EntrySent(t2) + c.EntrySent(t3) + c.EntrySent(t4) + + assert.Equal(t, 4, c.Size()) + + c.EntryReceived(t1) + c.EntryReceived(t2) + c.EntryReceived(t3) + + assert.Equal(t, 1, c.Size()) + + <-time.After(10 * time.Millisecond) + + expected := fmt.Sprintf(ErrEntryNotReceived, t4, 5*time.Millisecond.Seconds()) + + assert.Equal(t, expected, actual.String()) + assert.Equal(t, 0, c.Size()) + +} diff --git a/pkg/reader/logproto.pb.go b/pkg/reader/logproto.pb.go new file mode 100644 index 000000000000..a555d6d9087a --- /dev/null +++ b/pkg/reader/logproto.pb.go @@ -0,0 +1,2569 @@ +// FIXME Copied out of the loki project because of some import issues which caused this project to import all of cortex + +package reader + +import ( + context "context" + fmt "fmt" + _ "github.com/gogo/protobuf/gogoproto" + proto "github.com/gogo/protobuf/proto" + _ "github.com/gogo/protobuf/types" + github_com_gogo_protobuf_types "github.com/gogo/protobuf/types" + grpc "google.golang.org/grpc" + io "io" + math "math" + reflect "reflect" + strconv "strconv" + strings "strings" + time "time" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf +var _ = time.Kitchen + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package + +type Direction int32 + +const ( + FORWARD Direction = 0 + BACKWARD Direction = 1 +) + +var Direction_name = map[int32]string{ + 0: "FORWARD", + 1: "BACKWARD", +} + +var Direction_value = map[string]int32{ + "FORWARD": 0, + "BACKWARD": 1, +} + +func (Direction) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_7a8976f235a02f79, []int{0} +} + +type PushRequest struct { + Streams []*Stream `protobuf:"bytes,1,rep,name=streams,proto3" json:"streams"` +} + +func (m *PushRequest) Reset() { *m = PushRequest{} } +func (*PushRequest) ProtoMessage() {} +func (*PushRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_7a8976f235a02f79, []int{0} +} +func (m *PushRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *PushRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_PushRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalTo(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *PushRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_PushRequest.Merge(m, src) +} +func (m *PushRequest) XXX_Size() int { + return m.Size() +} +func (m *PushRequest) XXX_DiscardUnknown() { + xxx_messageInfo_PushRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_PushRequest proto.InternalMessageInfo + +func (m *PushRequest) GetStreams() []*Stream { + if m != nil { + return m.Streams + } + return nil +} + +type PushResponse struct { +} + +func (m *PushResponse) Reset() { *m = PushResponse{} } +func (*PushResponse) ProtoMessage() {} +func (*PushResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_7a8976f235a02f79, []int{1} +} +func (m *PushResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *PushResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_PushResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalTo(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *PushResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_PushResponse.Merge(m, src) +} +func (m *PushResponse) XXX_Size() int { + return m.Size() +} +func (m *PushResponse) XXX_DiscardUnknown() { + xxx_messageInfo_PushResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_PushResponse proto.InternalMessageInfo + +type QueryRequest struct { + Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + Limit uint32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + Start time.Time `protobuf:"bytes,3,opt,name=start,proto3,stdtime" json:"start"` + End time.Time `protobuf:"bytes,4,opt,name=end,proto3,stdtime" json:"end"` + Direction Direction `protobuf:"varint,5,opt,name=direction,proto3,enum=logproto.Direction" json:"direction,omitempty"` + Regex string `protobuf:"bytes,6,opt,name=regex,proto3" json:"regex,omitempty"` +} + +func (m *QueryRequest) Reset() { *m = QueryRequest{} } +func (*QueryRequest) ProtoMessage() {} +func (*QueryRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_7a8976f235a02f79, []int{2} +} +func (m *QueryRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalTo(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryRequest.Merge(m, src) +} +func (m *QueryRequest) XXX_Size() int { + return m.Size() +} +func (m *QueryRequest) XXX_DiscardUnknown() { + xxx_messageInfo_QueryRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryRequest proto.InternalMessageInfo + +func (m *QueryRequest) GetQuery() string { + if m != nil { + return m.Query + } + return "" +} + +func (m *QueryRequest) GetLimit() uint32 { + if m != nil { + return m.Limit + } + return 0 +} + +func (m *QueryRequest) GetStart() time.Time { + if m != nil { + return m.Start + } + return time.Time{} +} + +func (m *QueryRequest) GetEnd() time.Time { + if m != nil { + return m.End + } + return time.Time{} +} + +func (m *QueryRequest) GetDirection() Direction { + if m != nil { + return m.Direction + } + return FORWARD +} + +func (m *QueryRequest) GetRegex() string { + if m != nil { + return m.Regex + } + return "" +} + +type QueryResponse struct { + Streams []*Stream `protobuf:"bytes,1,rep,name=streams,proto3" json:"streams,omitempty"` +} + +func (m *QueryResponse) Reset() { *m = QueryResponse{} } +func (*QueryResponse) ProtoMessage() {} +func (*QueryResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_7a8976f235a02f79, []int{3} +} +func (m *QueryResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalTo(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryResponse.Merge(m, src) +} +func (m *QueryResponse) XXX_Size() int { + return m.Size() +} +func (m *QueryResponse) XXX_DiscardUnknown() { + xxx_messageInfo_QueryResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryResponse proto.InternalMessageInfo + +func (m *QueryResponse) GetStreams() []*Stream { + if m != nil { + return m.Streams + } + return nil +} + +type LabelRequest struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Values bool `protobuf:"varint,2,opt,name=values,proto3" json:"values,omitempty"` +} + +func (m *LabelRequest) Reset() { *m = LabelRequest{} } +func (*LabelRequest) ProtoMessage() {} +func (*LabelRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_7a8976f235a02f79, []int{4} +} +func (m *LabelRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *LabelRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_LabelRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalTo(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *LabelRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_LabelRequest.Merge(m, src) +} +func (m *LabelRequest) XXX_Size() int { + return m.Size() +} +func (m *LabelRequest) XXX_DiscardUnknown() { + xxx_messageInfo_LabelRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_LabelRequest proto.InternalMessageInfo + +func (m *LabelRequest) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *LabelRequest) GetValues() bool { + if m != nil { + return m.Values + } + return false +} + +type LabelResponse struct { + Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` +} + +func (m *LabelResponse) Reset() { *m = LabelResponse{} } +func (*LabelResponse) ProtoMessage() {} +func (*LabelResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_7a8976f235a02f79, []int{5} +} +func (m *LabelResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *LabelResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_LabelResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalTo(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *LabelResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_LabelResponse.Merge(m, src) +} +func (m *LabelResponse) XXX_Size() int { + return m.Size() +} +func (m *LabelResponse) XXX_DiscardUnknown() { + xxx_messageInfo_LabelResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_LabelResponse proto.InternalMessageInfo + +func (m *LabelResponse) GetValues() []string { + if m != nil { + return m.Values + } + return nil +} + +type Stream struct { + Labels string `protobuf:"bytes,1,opt,name=labels,proto3" json:"labels"` + Entries []Entry `protobuf:"bytes,2,rep,name=entries,proto3" json:"entries"` +} + +func (m *Stream) Reset() { *m = Stream{} } +func (*Stream) ProtoMessage() {} +func (*Stream) Descriptor() ([]byte, []int) { + return fileDescriptor_7a8976f235a02f79, []int{6} +} +func (m *Stream) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Stream) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Stream.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalTo(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Stream) XXX_Merge(src proto.Message) { + xxx_messageInfo_Stream.Merge(m, src) +} +func (m *Stream) XXX_Size() int { + return m.Size() +} +func (m *Stream) XXX_DiscardUnknown() { + xxx_messageInfo_Stream.DiscardUnknown(m) +} + +var xxx_messageInfo_Stream proto.InternalMessageInfo + +func (m *Stream) GetLabels() string { + if m != nil { + return m.Labels + } + return "" +} + +func (m *Stream) GetEntries() []Entry { + if m != nil { + return m.Entries + } + return nil +} + +type Entry struct { + Timestamp time.Time `protobuf:"bytes,1,opt,name=timestamp,proto3,stdtime" json:"ts"` + Line string `protobuf:"bytes,2,opt,name=line,proto3" json:"line"` +} + +func (m *Entry) Reset() { *m = Entry{} } +func (*Entry) ProtoMessage() {} +func (*Entry) Descriptor() ([]byte, []int) { + return fileDescriptor_7a8976f235a02f79, []int{7} +} +func (m *Entry) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Entry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Entry.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalTo(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Entry) XXX_Merge(src proto.Message) { + xxx_messageInfo_Entry.Merge(m, src) +} +func (m *Entry) XXX_Size() int { + return m.Size() +} +func (m *Entry) XXX_DiscardUnknown() { + xxx_messageInfo_Entry.DiscardUnknown(m) +} + +var xxx_messageInfo_Entry proto.InternalMessageInfo + +func (m *Entry) GetTimestamp() time.Time { + if m != nil { + return m.Timestamp + } + return time.Time{} +} + +func (m *Entry) GetLine() string { + if m != nil { + return m.Line + } + return "" +} + +func init() { + proto.RegisterEnum("logproto.Direction", Direction_name, Direction_value) + proto.RegisterType((*PushRequest)(nil), "logproto.PushRequest") + proto.RegisterType((*PushResponse)(nil), "logproto.PushResponse") + proto.RegisterType((*QueryRequest)(nil), "logproto.QueryRequest") + proto.RegisterType((*QueryResponse)(nil), "logproto.QueryResponse") + proto.RegisterType((*LabelRequest)(nil), "logproto.LabelRequest") + proto.RegisterType((*LabelResponse)(nil), "logproto.LabelResponse") + proto.RegisterType((*Stream)(nil), "logproto.Stream") + proto.RegisterType((*Entry)(nil), "logproto.Entry") +} + +func init() { proto.RegisterFile("logproto.proto", fileDescriptor_7a8976f235a02f79) } + +var fileDescriptor_7a8976f235a02f79 = []byte{ + // 601 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x53, 0x4f, 0x6f, 0xd3, 0x4e, + 0x10, 0xf5, 0xb6, 0x89, 0x13, 0x4f, 0xd2, 0xb4, 0xda, 0xdf, 0x8f, 0x62, 0x45, 0x68, 0x1d, 0xf9, + 0x00, 0x51, 0x25, 0x5c, 0x08, 0x88, 0x4a, 0x85, 0x4b, 0x4d, 0xa9, 0x90, 0x40, 0x02, 0x16, 0x24, + 0xce, 0x4e, 0xbb, 0xb8, 0x96, 0xfc, 0xa7, 0xb5, 0xd7, 0x88, 0xde, 0x90, 0xf8, 0x02, 0xfd, 0x18, + 0x7c, 0x94, 0x1e, 0x73, 0xec, 0x29, 0x10, 0xe7, 0x82, 0x72, 0xea, 0x8d, 0x2b, 0xda, 0xb5, 0x1d, + 0x1b, 0x90, 0x40, 0x5c, 0x9c, 0x79, 0xbb, 0xef, 0xcd, 0xec, 0x9b, 0x99, 0x40, 0xcf, 0x8f, 0xdc, + 0x93, 0x38, 0xe2, 0x91, 0x25, 0xbf, 0xb8, 0x5d, 0xe2, 0xbe, 0xe1, 0x46, 0x91, 0xeb, 0xb3, 0x6d, + 0x89, 0xc6, 0xe9, 0xbb, 0x6d, 0xee, 0x05, 0x2c, 0xe1, 0x4e, 0x70, 0x92, 0x53, 0xfb, 0xb7, 0x5d, + 0x8f, 0x1f, 0xa7, 0x63, 0xeb, 0x30, 0x0a, 0xb6, 0xdd, 0xc8, 0x8d, 0x2a, 0xa6, 0x40, 0x12, 0xc8, + 0x28, 0xa7, 0x9b, 0x07, 0xd0, 0x79, 0x99, 0x26, 0xc7, 0x94, 0x9d, 0xa6, 0x2c, 0xe1, 0x78, 0x07, + 0x5a, 0x09, 0x8f, 0x99, 0x13, 0x24, 0x3a, 0x1a, 0xac, 0x0e, 0x3b, 0xa3, 0x0d, 0x6b, 0xf9, 0x94, + 0xd7, 0xf2, 0xc2, 0xee, 0x2c, 0xa6, 0x46, 0x49, 0xa2, 0x65, 0x60, 0xf6, 0xa0, 0x9b, 0xe7, 0x49, + 0x4e, 0xa2, 0x30, 0x61, 0xe6, 0x77, 0x04, 0xdd, 0x57, 0x29, 0x8b, 0xcf, 0xca, 0xcc, 0xff, 0x43, + 0xf3, 0x54, 0x60, 0x1d, 0x0d, 0xd0, 0x50, 0xa3, 0x39, 0x10, 0xa7, 0xbe, 0x17, 0x78, 0x5c, 0x5f, + 0x19, 0xa0, 0xe1, 0x1a, 0xcd, 0x01, 0xde, 0x85, 0x66, 0xc2, 0x9d, 0x98, 0xeb, 0xab, 0x03, 0x34, + 0xec, 0x8c, 0xfa, 0x56, 0x6e, 0xda, 0x2a, 0xad, 0x58, 0x6f, 0x4a, 0xd3, 0x76, 0xfb, 0x62, 0x6a, + 0x28, 0xe7, 0x5f, 0x0c, 0x44, 0x73, 0x09, 0x7e, 0x00, 0xab, 0x2c, 0x3c, 0xd2, 0x1b, 0xff, 0xa0, + 0x14, 0x02, 0x7c, 0x17, 0xb4, 0x23, 0x2f, 0x66, 0x87, 0xdc, 0x8b, 0x42, 0xbd, 0x39, 0x40, 0xc3, + 0xde, 0xe8, 0xbf, 0xca, 0xfb, 0x7e, 0x79, 0x45, 0x2b, 0x96, 0x78, 0x7c, 0xcc, 0x5c, 0xf6, 0x41, + 0x57, 0x73, 0x4b, 0x12, 0x98, 0x0f, 0x61, 0xad, 0x30, 0x9e, 0xb7, 0x02, 0x6f, 0xfd, 0xb5, 0xa7, + 0x55, 0x1b, 0x77, 0xa1, 0xfb, 0xdc, 0x19, 0x33, 0xbf, 0xec, 0x1a, 0x86, 0x46, 0xe8, 0x04, 0xac, + 0x68, 0x9a, 0x8c, 0xf1, 0x26, 0xa8, 0xef, 0x1d, 0x3f, 0x65, 0x89, 0x6c, 0x5a, 0x9b, 0x16, 0xc8, + 0xbc, 0x05, 0x6b, 0x85, 0xb6, 0x28, 0x5c, 0x11, 0x45, 0x5d, 0x6d, 0x49, 0x3c, 0x06, 0x35, 0xaf, + 0x8b, 0x4d, 0x50, 0x7d, 0x21, 0x49, 0xf2, 0x02, 0x36, 0x2c, 0xa6, 0x46, 0x71, 0x42, 0x8b, 0x5f, + 0xbc, 0x0b, 0x2d, 0x16, 0xf2, 0xd8, 0x93, 0xf5, 0xc4, 0xf3, 0xd7, 0xab, 0xe7, 0x3f, 0x09, 0x79, + 0x7c, 0x66, 0xaf, 0x8b, 0x4e, 0x8a, 0xad, 0x28, 0x78, 0xb4, 0x0c, 0xcc, 0x08, 0x9a, 0x92, 0x82, + 0x9f, 0x82, 0xb6, 0x5c, 0x54, 0x59, 0xeb, 0xcf, 0xb3, 0xe9, 0x15, 0x19, 0x57, 0x78, 0x22, 0x27, + 0x54, 0x89, 0xf1, 0x0d, 0x68, 0xf8, 0x5e, 0xc8, 0xa4, 0x77, 0xcd, 0x6e, 0x2f, 0xa6, 0x86, 0xc4, + 0x54, 0x7e, 0xb7, 0x6e, 0x82, 0xb6, 0x1c, 0x15, 0xee, 0x40, 0xeb, 0xe0, 0x05, 0x7d, 0xbb, 0x47, + 0xf7, 0x37, 0x14, 0xdc, 0x85, 0xb6, 0xbd, 0xf7, 0xf8, 0x99, 0x44, 0x68, 0xb4, 0x07, 0xaa, 0x58, + 0x57, 0x16, 0xe3, 0x1d, 0x68, 0x88, 0x08, 0x5f, 0xab, 0x5c, 0xd5, 0xfe, 0x10, 0xfd, 0xcd, 0x5f, + 0x8f, 0x8b, 0xfd, 0x56, 0x46, 0x9f, 0x10, 0xb4, 0xc4, 0xa0, 0x3d, 0x16, 0xe3, 0x47, 0xd0, 0x94, + 0x33, 0xc7, 0x35, 0x7a, 0x7d, 0xfb, 0xfb, 0xd7, 0x7f, 0x3b, 0x2f, 0xf3, 0xdc, 0x41, 0x62, 0xdd, + 0xe5, 0xe0, 0xea, 0xea, 0xfa, 0x16, 0xd4, 0xd5, 0x3f, 0x4d, 0xd8, 0x54, 0xec, 0xfb, 0x93, 0x19, + 0x51, 0x2e, 0x67, 0x44, 0xb9, 0x9a, 0x11, 0xf4, 0x31, 0x23, 0xe8, 0x73, 0x46, 0xd0, 0x45, 0x46, + 0xd0, 0x24, 0x23, 0xe8, 0x6b, 0x46, 0xd0, 0xb7, 0x8c, 0x28, 0x57, 0x19, 0x41, 0xe7, 0x73, 0xa2, + 0x4c, 0xe6, 0x44, 0xb9, 0x9c, 0x13, 0x65, 0xac, 0xca, 0x64, 0xf7, 0x7e, 0x04, 0x00, 0x00, 0xff, + 0xff, 0x47, 0x69, 0x1e, 0x88, 0x68, 0x04, 0x00, 0x00, +} + +func (x Direction) String() string { + s, ok := Direction_name[int32(x)] + if ok { + return s + } + return strconv.Itoa(int(x)) +} +func (this *PushRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*PushRequest) + if !ok { + that2, ok := that.(PushRequest) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if len(this.Streams) != len(that1.Streams) { + return false + } + for i := range this.Streams { + if !this.Streams[i].Equal(that1.Streams[i]) { + return false + } + } + return true +} +func (this *PushResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*PushResponse) + if !ok { + that2, ok := that.(PushResponse) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + return true +} +func (this *QueryRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*QueryRequest) + if !ok { + that2, ok := that.(QueryRequest) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if this.Query != that1.Query { + return false + } + if this.Limit != that1.Limit { + return false + } + if !this.Start.Equal(that1.Start) { + return false + } + if !this.End.Equal(that1.End) { + return false + } + if this.Direction != that1.Direction { + return false + } + if this.Regex != that1.Regex { + return false + } + return true +} +func (this *QueryResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*QueryResponse) + if !ok { + that2, ok := that.(QueryResponse) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if len(this.Streams) != len(that1.Streams) { + return false + } + for i := range this.Streams { + if !this.Streams[i].Equal(that1.Streams[i]) { + return false + } + } + return true +} +func (this *LabelRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*LabelRequest) + if !ok { + that2, ok := that.(LabelRequest) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if this.Name != that1.Name { + return false + } + if this.Values != that1.Values { + return false + } + return true +} +func (this *LabelResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*LabelResponse) + if !ok { + that2, ok := that.(LabelResponse) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if len(this.Values) != len(that1.Values) { + return false + } + for i := range this.Values { + if this.Values[i] != that1.Values[i] { + return false + } + } + return true +} +func (this *Stream) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*Stream) + if !ok { + that2, ok := that.(Stream) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if this.Labels != that1.Labels { + return false + } + if len(this.Entries) != len(that1.Entries) { + return false + } + for i := range this.Entries { + if !this.Entries[i].Equal(&that1.Entries[i]) { + return false + } + } + return true +} +func (this *Entry) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*Entry) + if !ok { + that2, ok := that.(Entry) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if !this.Timestamp.Equal(that1.Timestamp) { + return false + } + if this.Line != that1.Line { + return false + } + return true +} +func (this *PushRequest) GoString() string { + if this == nil { + return "nil" + } + s := make([]string, 0, 5) + s = append(s, "&logproto.PushRequest{") + if this.Streams != nil { + s = append(s, "Streams: "+fmt.Sprintf("%#v", this.Streams)+",\n") + } + s = append(s, "}") + return strings.Join(s, "") +} +func (this *PushResponse) GoString() string { + if this == nil { + return "nil" + } + s := make([]string, 0, 4) + s = append(s, "&logproto.PushResponse{") + s = append(s, "}") + return strings.Join(s, "") +} +func (this *QueryRequest) GoString() string { + if this == nil { + return "nil" + } + s := make([]string, 0, 10) + s = append(s, "&logproto.QueryRequest{") + s = append(s, "Query: "+fmt.Sprintf("%#v", this.Query)+",\n") + s = append(s, "Limit: "+fmt.Sprintf("%#v", this.Limit)+",\n") + s = append(s, "Start: "+fmt.Sprintf("%#v", this.Start)+",\n") + s = append(s, "End: "+fmt.Sprintf("%#v", this.End)+",\n") + s = append(s, "Direction: "+fmt.Sprintf("%#v", this.Direction)+",\n") + s = append(s, "Regex: "+fmt.Sprintf("%#v", this.Regex)+",\n") + s = append(s, "}") + return strings.Join(s, "") +} +func (this *QueryResponse) GoString() string { + if this == nil { + return "nil" + } + s := make([]string, 0, 5) + s = append(s, "&logproto.QueryResponse{") + if this.Streams != nil { + s = append(s, "Streams: "+fmt.Sprintf("%#v", this.Streams)+",\n") + } + s = append(s, "}") + return strings.Join(s, "") +} +func (this *LabelRequest) GoString() string { + if this == nil { + return "nil" + } + s := make([]string, 0, 6) + s = append(s, "&logproto.LabelRequest{") + s = append(s, "Name: "+fmt.Sprintf("%#v", this.Name)+",\n") + s = append(s, "Values: "+fmt.Sprintf("%#v", this.Values)+",\n") + s = append(s, "}") + return strings.Join(s, "") +} +func (this *LabelResponse) GoString() string { + if this == nil { + return "nil" + } + s := make([]string, 0, 5) + s = append(s, "&logproto.LabelResponse{") + s = append(s, "Values: "+fmt.Sprintf("%#v", this.Values)+",\n") + s = append(s, "}") + return strings.Join(s, "") +} +func (this *Stream) GoString() string { + if this == nil { + return "nil" + } + s := make([]string, 0, 6) + s = append(s, "&logproto.Stream{") + s = append(s, "Labels: "+fmt.Sprintf("%#v", this.Labels)+",\n") + if this.Entries != nil { + vs := make([]*Entry, len(this.Entries)) + for i := range vs { + vs[i] = &this.Entries[i] + } + s = append(s, "Entries: "+fmt.Sprintf("%#v", vs)+",\n") + } + s = append(s, "}") + return strings.Join(s, "") +} +func (this *Entry) GoString() string { + if this == nil { + return "nil" + } + s := make([]string, 0, 6) + s = append(s, "&logproto.Entry{") + s = append(s, "Timestamp: "+fmt.Sprintf("%#v", this.Timestamp)+",\n") + s = append(s, "Line: "+fmt.Sprintf("%#v", this.Line)+",\n") + s = append(s, "}") + return strings.Join(s, "") +} +func valueToGoStringLogproto(v interface{}, typ string) string { + rv := reflect.ValueOf(v) + if rv.IsNil() { + return "nil" + } + pv := reflect.Indirect(rv).Interface() + return fmt.Sprintf("func(v %v) *%v { return &v } ( %#v )", typ, typ, pv) +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// PusherClient is the client API for Pusher service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type PusherClient interface { + Push(ctx context.Context, in *PushRequest, opts ...grpc.CallOption) (*PushResponse, error) +} + +type pusherClient struct { + cc *grpc.ClientConn +} + +func NewPusherClient(cc *grpc.ClientConn) PusherClient { + return &pusherClient{cc} +} + +func (c *pusherClient) Push(ctx context.Context, in *PushRequest, opts ...grpc.CallOption) (*PushResponse, error) { + out := new(PushResponse) + err := c.cc.Invoke(ctx, "/logproto.Pusher/Push", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// PusherServer is the server API for Pusher service. +type PusherServer interface { + Push(context.Context, *PushRequest) (*PushResponse, error) +} + +func RegisterPusherServer(s *grpc.Server, srv PusherServer) { + s.RegisterService(&_Pusher_serviceDesc, srv) +} + +func _Pusher_Push_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PushRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PusherServer).Push(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/logproto.Pusher/Push", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PusherServer).Push(ctx, req.(*PushRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _Pusher_serviceDesc = grpc.ServiceDesc{ + ServiceName: "logproto.Pusher", + HandlerType: (*PusherServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Push", + Handler: _Pusher_Push_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "logproto.proto", +} + +// QuerierClient is the client API for Querier service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type QuerierClient interface { + Query(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (Querier_QueryClient, error) + Label(ctx context.Context, in *LabelRequest, opts ...grpc.CallOption) (*LabelResponse, error) +} + +type querierClient struct { + cc *grpc.ClientConn +} + +func NewQuerierClient(cc *grpc.ClientConn) QuerierClient { + return &querierClient{cc} +} + +func (c *querierClient) Query(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (Querier_QueryClient, error) { + stream, err := c.cc.NewStream(ctx, &_Querier_serviceDesc.Streams[0], "/logproto.Querier/Query", opts...) + if err != nil { + return nil, err + } + x := &querierQueryClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type Querier_QueryClient interface { + Recv() (*QueryResponse, error) + grpc.ClientStream +} + +type querierQueryClient struct { + grpc.ClientStream +} + +func (x *querierQueryClient) Recv() (*QueryResponse, error) { + m := new(QueryResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *querierClient) Label(ctx context.Context, in *LabelRequest, opts ...grpc.CallOption) (*LabelResponse, error) { + out := new(LabelResponse) + err := c.cc.Invoke(ctx, "/logproto.Querier/Label", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// QuerierServer is the server API for Querier service. +type QuerierServer interface { + Query(*QueryRequest, Querier_QueryServer) error + Label(context.Context, *LabelRequest) (*LabelResponse, error) +} + +func RegisterQuerierServer(s *grpc.Server, srv QuerierServer) { + s.RegisterService(&_Querier_serviceDesc, srv) +} + +func _Querier_Query_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(QueryRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(QuerierServer).Query(m, &querierQueryServer{stream}) +} + +type Querier_QueryServer interface { + Send(*QueryResponse) error + grpc.ServerStream +} + +type querierQueryServer struct { + grpc.ServerStream +} + +func (x *querierQueryServer) Send(m *QueryResponse) error { + return x.ServerStream.SendMsg(m) +} + +func _Querier_Label_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LabelRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QuerierServer).Label(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/logproto.Querier/Label", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QuerierServer).Label(ctx, req.(*LabelRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _Querier_serviceDesc = grpc.ServiceDesc{ + ServiceName: "logproto.Querier", + HandlerType: (*QuerierServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Label", + Handler: _Querier_Label_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "Query", + Handler: _Querier_Query_Handler, + ServerStreams: true, + }, + }, + Metadata: "logproto.proto", +} + +func (m *PushRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *PushRequest) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.Streams) > 0 { + for _, msg := range m.Streams { + dAtA[i] = 0xa + i++ + i = encodeVarintLogproto(dAtA, i, uint64(msg.Size())) + n, err := msg.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n + } + } + return i, nil +} + +func (m *PushResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *PushResponse) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + return i, nil +} + +func (m *QueryRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryRequest) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.Query) > 0 { + dAtA[i] = 0xa + i++ + i = encodeVarintLogproto(dAtA, i, uint64(len(m.Query))) + i += copy(dAtA[i:], m.Query) + } + if m.Limit != 0 { + dAtA[i] = 0x10 + i++ + i = encodeVarintLogproto(dAtA, i, uint64(m.Limit)) + } + dAtA[i] = 0x1a + i++ + i = encodeVarintLogproto(dAtA, i, uint64(github_com_gogo_protobuf_types.SizeOfStdTime(m.Start))) + n1, err1 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.Start, dAtA[i:]) + if err1 != nil { + return 0, err1 + } + i += n1 + dAtA[i] = 0x22 + i++ + i = encodeVarintLogproto(dAtA, i, uint64(github_com_gogo_protobuf_types.SizeOfStdTime(m.End))) + n2, err2 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.End, dAtA[i:]) + if err2 != nil { + return 0, err2 + } + i += n2 + if m.Direction != 0 { + dAtA[i] = 0x28 + i++ + i = encodeVarintLogproto(dAtA, i, uint64(m.Direction)) + } + if len(m.Regex) > 0 { + dAtA[i] = 0x32 + i++ + i = encodeVarintLogproto(dAtA, i, uint64(len(m.Regex))) + i += copy(dAtA[i:], m.Regex) + } + return i, nil +} + +func (m *QueryResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryResponse) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.Streams) > 0 { + for _, msg := range m.Streams { + dAtA[i] = 0xa + i++ + i = encodeVarintLogproto(dAtA, i, uint64(msg.Size())) + n, err := msg.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n + } + } + return i, nil +} + +func (m *LabelRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *LabelRequest) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.Name) > 0 { + dAtA[i] = 0xa + i++ + i = encodeVarintLogproto(dAtA, i, uint64(len(m.Name))) + i += copy(dAtA[i:], m.Name) + } + if m.Values { + dAtA[i] = 0x10 + i++ + if m.Values { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i++ + } + return i, nil +} + +func (m *LabelResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *LabelResponse) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.Values) > 0 { + for _, s := range m.Values { + dAtA[i] = 0xa + i++ + l = len(s) + for l >= 1<<7 { + dAtA[i] = uint8(uint64(l)&0x7f | 0x80) + l >>= 7 + i++ + } + dAtA[i] = uint8(l) + i++ + i += copy(dAtA[i:], s) + } + } + return i, nil +} + +func (m *Stream) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Stream) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.Labels) > 0 { + dAtA[i] = 0xa + i++ + i = encodeVarintLogproto(dAtA, i, uint64(len(m.Labels))) + i += copy(dAtA[i:], m.Labels) + } + if len(m.Entries) > 0 { + for _, msg := range m.Entries { + dAtA[i] = 0x12 + i++ + i = encodeVarintLogproto(dAtA, i, uint64(msg.Size())) + n, err := msg.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n + } + } + return i, nil +} + +func (m *Entry) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Entry) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + dAtA[i] = 0xa + i++ + i = encodeVarintLogproto(dAtA, i, uint64(github_com_gogo_protobuf_types.SizeOfStdTime(m.Timestamp))) + n3, err3 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.Timestamp, dAtA[i:]) + if err3 != nil { + return 0, err3 + } + i += n3 + if len(m.Line) > 0 { + dAtA[i] = 0x12 + i++ + i = encodeVarintLogproto(dAtA, i, uint64(len(m.Line))) + i += copy(dAtA[i:], m.Line) + } + return i, nil +} + +func encodeVarintLogproto(dAtA []byte, offset int, v uint64) int { + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return offset + 1 +} +func (m *PushRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Streams) > 0 { + for _, e := range m.Streams { + l = e.Size() + n += 1 + l + sovLogproto(uint64(l)) + } + } + return n +} + +func (m *PushResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + +func (m *QueryRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Query) + if l > 0 { + n += 1 + l + sovLogproto(uint64(l)) + } + if m.Limit != 0 { + n += 1 + sovLogproto(uint64(m.Limit)) + } + l = github_com_gogo_protobuf_types.SizeOfStdTime(m.Start) + n += 1 + l + sovLogproto(uint64(l)) + l = github_com_gogo_protobuf_types.SizeOfStdTime(m.End) + n += 1 + l + sovLogproto(uint64(l)) + if m.Direction != 0 { + n += 1 + sovLogproto(uint64(m.Direction)) + } + l = len(m.Regex) + if l > 0 { + n += 1 + l + sovLogproto(uint64(l)) + } + return n +} + +func (m *QueryResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Streams) > 0 { + for _, e := range m.Streams { + l = e.Size() + n += 1 + l + sovLogproto(uint64(l)) + } + } + return n +} + +func (m *LabelRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Name) + if l > 0 { + n += 1 + l + sovLogproto(uint64(l)) + } + if m.Values { + n += 2 + } + return n +} + +func (m *LabelResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Values) > 0 { + for _, s := range m.Values { + l = len(s) + n += 1 + l + sovLogproto(uint64(l)) + } + } + return n +} + +func (m *Stream) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Labels) + if l > 0 { + n += 1 + l + sovLogproto(uint64(l)) + } + if len(m.Entries) > 0 { + for _, e := range m.Entries { + l = e.Size() + n += 1 + l + sovLogproto(uint64(l)) + } + } + return n +} + +func (m *Entry) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = github_com_gogo_protobuf_types.SizeOfStdTime(m.Timestamp) + n += 1 + l + sovLogproto(uint64(l)) + l = len(m.Line) + if l > 0 { + n += 1 + l + sovLogproto(uint64(l)) + } + return n +} + +func sovLogproto(x uint64) (n int) { + for { + n++ + x >>= 7 + if x == 0 { + break + } + } + return n +} +func sozLogproto(x uint64) (n int) { + return sovLogproto(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (this *PushRequest) String() string { + if this == nil { + return "nil" + } + repeatedStringForStreams := "[]*Stream{" + for _, f := range this.Streams { + repeatedStringForStreams += strings.Replace(f.String(), "Stream", "Stream", 1) + "," + } + repeatedStringForStreams += "}" + s := strings.Join([]string{`&PushRequest{`, + `Streams:` + repeatedStringForStreams + `,`, + `}`, + }, "") + return s +} +func (this *PushResponse) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&PushResponse{`, + `}`, + }, "") + return s +} +func (this *QueryRequest) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&QueryRequest{`, + `Query:` + fmt.Sprintf("%v", this.Query) + `,`, + `Limit:` + fmt.Sprintf("%v", this.Limit) + `,`, + `Start:` + strings.Replace(strings.Replace(fmt.Sprintf("%v", this.Start), "Timestamp", "types.Timestamp", 1), `&`, ``, 1) + `,`, + `End:` + strings.Replace(strings.Replace(fmt.Sprintf("%v", this.End), "Timestamp", "types.Timestamp", 1), `&`, ``, 1) + `,`, + `Direction:` + fmt.Sprintf("%v", this.Direction) + `,`, + `Regex:` + fmt.Sprintf("%v", this.Regex) + `,`, + `}`, + }, "") + return s +} +func (this *QueryResponse) String() string { + if this == nil { + return "nil" + } + repeatedStringForStreams := "[]*Stream{" + for _, f := range this.Streams { + repeatedStringForStreams += strings.Replace(f.String(), "Stream", "Stream", 1) + "," + } + repeatedStringForStreams += "}" + s := strings.Join([]string{`&QueryResponse{`, + `Streams:` + repeatedStringForStreams + `,`, + `}`, + }, "") + return s +} +func (this *LabelRequest) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&LabelRequest{`, + `Name:` + fmt.Sprintf("%v", this.Name) + `,`, + `Values:` + fmt.Sprintf("%v", this.Values) + `,`, + `}`, + }, "") + return s +} +func (this *LabelResponse) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&LabelResponse{`, + `Values:` + fmt.Sprintf("%v", this.Values) + `,`, + `}`, + }, "") + return s +} +func (this *Stream) String() string { + if this == nil { + return "nil" + } + repeatedStringForEntries := "[]Entry{" + for _, f := range this.Entries { + repeatedStringForEntries += strings.Replace(strings.Replace(f.String(), "Entry", "Entry", 1), `&`, ``, 1) + "," + } + repeatedStringForEntries += "}" + s := strings.Join([]string{`&Stream{`, + `Labels:` + fmt.Sprintf("%v", this.Labels) + `,`, + `Entries:` + repeatedStringForEntries + `,`, + `}`, + }, "") + return s +} +func (this *Entry) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&Entry{`, + `Timestamp:` + strings.Replace(strings.Replace(fmt.Sprintf("%v", this.Timestamp), "Timestamp", "types.Timestamp", 1), `&`, ``, 1) + `,`, + `Line:` + fmt.Sprintf("%v", this.Line) + `,`, + `}`, + }, "") + return s +} +func valueToStringLogproto(v interface{}) string { + rv := reflect.ValueOf(v) + if rv.IsNil() { + return "nil" + } + pv := reflect.Indirect(rv).Interface() + return fmt.Sprintf("*%v", pv) +} +func (m *PushRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: PushRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: PushRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Streams", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthLogproto + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthLogproto + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Streams = append(m.Streams, &Stream{}) + if err := m.Streams[len(m.Streams)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipLogproto(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthLogproto + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthLogproto + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *PushResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: PushResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: PushResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipLogproto(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthLogproto + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthLogproto + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *QueryRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Query", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthLogproto + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthLogproto + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Query = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Limit", wireType) + } + m.Limit = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Limit |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Start", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthLogproto + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthLogproto + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.Start, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field End", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthLogproto + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthLogproto + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.End, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Direction", wireType) + } + m.Direction = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Direction |= Direction(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Regex", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthLogproto + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthLogproto + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Regex = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipLogproto(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthLogproto + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthLogproto + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *QueryResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Streams", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthLogproto + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthLogproto + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Streams = append(m.Streams, &Stream{}) + if err := m.Streams[len(m.Streams)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipLogproto(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthLogproto + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthLogproto + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *LabelRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: LabelRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: LabelRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthLogproto + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthLogproto + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Name = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Values", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Values = bool(v != 0) + default: + iNdEx = preIndex + skippy, err := skipLogproto(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthLogproto + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthLogproto + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *LabelResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: LabelResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: LabelResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Values", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthLogproto + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthLogproto + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Values = append(m.Values, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipLogproto(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthLogproto + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthLogproto + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Stream) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Stream: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Stream: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Labels", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthLogproto + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthLogproto + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Labels = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Entries", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthLogproto + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthLogproto + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Entries = append(m.Entries, Entry{}) + if err := m.Entries[len(m.Entries)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipLogproto(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthLogproto + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthLogproto + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Entry) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Entry: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Entry: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthLogproto + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthLogproto + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.Timestamp, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Line", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLogproto + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthLogproto + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthLogproto + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Line = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipLogproto(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthLogproto + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthLogproto + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipLogproto(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowLogproto + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowLogproto + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + return iNdEx, nil + case 1: + iNdEx += 8 + return iNdEx, nil + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowLogproto + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthLogproto + } + iNdEx += length + if iNdEx < 0 { + return 0, ErrInvalidLengthLogproto + } + return iNdEx, nil + case 3: + for { + var innerWire uint64 + var start int = iNdEx + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowLogproto + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + innerWire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + innerWireType := int(innerWire & 0x7) + if innerWireType == 4 { + break + } + next, err := skipLogproto(dAtA[start:]) + if err != nil { + return 0, err + } + iNdEx = start + next + if iNdEx < 0 { + return 0, ErrInvalidLengthLogproto + } + } + return iNdEx, nil + case 4: + return iNdEx, nil + case 5: + iNdEx += 4 + return iNdEx, nil + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + } + panic("unreachable") +} + +var ( + ErrInvalidLengthLogproto = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowLogproto = fmt.Errorf("proto: integer overflow") +) diff --git a/pkg/reader/reader.go b/pkg/reader/reader.go new file mode 100644 index 000000000000..026441436dfd --- /dev/null +++ b/pkg/reader/reader.go @@ -0,0 +1,113 @@ +package reader + +import ( + "encoding/base64" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/gorilla/websocket" + + "github.com/grafana/loki-canary/pkg/comparator" +) + +type Reader struct { + url url.URL + header http.Header + conn *websocket.Conn + w io.Writer + cm *comparator.Comparator + quit chan struct{} + shuttingDown bool + done chan struct{} +} + +func NewReader(writer io.Writer, comparator *comparator.Comparator, url url.URL, user string, pass string) *Reader { + h := http.Header{} + if user != "" { + h = http.Header{"Authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+pass))}} + } + rd := Reader{ + w: writer, + url: url, + cm: comparator, + header: h, + quit: make(chan struct{}), + done: make(chan struct{}), + shuttingDown: false, + } + + go rd.run() + + go func() { + select { + case <-rd.quit: + if rd.conn != nil { + _, _ = fmt.Fprintf(rd.w, "shutting down reader\n") + rd.shuttingDown = true + _ = rd.conn.Close() + } + } + }() + + return &rd +} + +func (r *Reader) Stop() { + close(r.quit) + <-r.done +} + +func (r *Reader) run() { + + r.closeAndReconnect() + + stream := &Stream{} + + for { + err := r.conn.ReadJSON(stream) + if err != nil { + if r.shuttingDown { + close(r.done) + return + } + _, _ = fmt.Fprintf(r.w, "error reading websocket: %s\n", err) + r.closeAndReconnect() + continue + } + + for _, entry := range stream.Entries { + sp := strings.Split(entry.Line, " ") + if len(sp) != 2 { + _, _ = fmt.Fprintf(r.w, "received invalid entry: %s\n", entry.Line) + continue + } + ts, err := strconv.ParseInt(sp[0], 10, 64) + if err != nil { + _, _ = fmt.Fprintf(r.w, "failed to parse timestamp: %s\n", sp[0]) + continue + } + r.cm.EntryReceived(time.Unix(0, ts)) + } + } +} + +func (r *Reader) closeAndReconnect() { + if r.conn != nil { + _ = r.conn.Close() + r.conn = nil + } + for r.conn == nil { + c, _, err := websocket.DefaultDialer.Dial(r.url.String(), r.header) + if err != nil { + _, _ = fmt.Fprintf(r.w, "failed to connect to %s with err %s\n", r.url.String(), err) + <-time.After(5 * time.Second) + continue + } + r.conn = c + } +} diff --git a/pkg/writer/writer.go b/pkg/writer/writer.go new file mode 100644 index 000000000000..addb28e420d4 --- /dev/null +++ b/pkg/writer/writer.go @@ -0,0 +1,81 @@ +package writer + +import ( + "fmt" + "io" + "strconv" + "strings" + "time" + + "github.com/grafana/loki-canary/pkg/comparator" +) + +const ( + LogEntry = "%s %s\n" +) + +type Writer struct { + w io.Writer + cm *comparator.Comparator + interval time.Duration + size int + prevTsLen int + pad string + quit chan struct{} + done chan struct{} +} + +func NewWriter(writer io.Writer, comparator *comparator.Comparator, entryInterval time.Duration, entrySize int) *Writer { + + w := &Writer{ + w: writer, + cm: comparator, + interval: entryInterval, + size: entrySize, + prevTsLen: 0, + quit: make(chan struct{}), + done: make(chan struct{}), + } + + go w.run() + + return w +} + +func (w *Writer) Stop() { + close(w.quit) + <-w.done +} + +func (w *Writer) run() { + t := time.NewTicker(w.interval) + defer func() { + t.Stop() + close(w.done) + }() + for { + select { + case <-t.C: + t := time.Now() + ts := strconv.FormatInt(t.UnixNano(), 10) + tsLen := len(ts) + + // I guess some day this could happen???? + if w.prevTsLen != tsLen { + var str strings.Builder + // Total line length includes timestamp, white space separator, new line char. Subtract those out + for str.Len() < w.size-tsLen-2 { + str.WriteString("p") + } + w.pad = str.String() + w.prevTsLen = tsLen + } + + _, _ = fmt.Fprintf(w.w, LogEntry, ts, w.pad) + w.cm.EntrySent(t) + case <-w.quit: + return + } + } + +} From 45a7556da20f635c45370bc8a2261a15b4c7a8d8 Mon Sep 17 00:00:00 2001 From: Edward Welch Date: Mon, 13 May 2019 23:32:43 -0400 Subject: [PATCH 02/17] improving metrics improving tests config via flags added ksonnet config --- cmd/loki-canary/main.go | 53 +++++++++++++-- go.mod | 1 + pkg/comparator/comparator.go | 42 +++++++++--- pkg/comparator/comparator_test.go | 66 ++++++++++++++++++- pkg/reader/logproto.pb.go | 21 +++--- .../ksonnet/loki-canary/jsonnetfile.json | 14 ++++ .../ksonnet/loki-canary/loki-canary.libsonnet | 46 +++++++++++++ 7 files changed, 217 insertions(+), 26 deletions(-) create mode 100644 production/ksonnet/loki-canary/jsonnetfile.json create mode 100644 production/ksonnet/loki-canary/loki-canary.libsonnet diff --git a/cmd/loki-canary/main.go b/cmd/loki-canary/main.go index 40dab0d4701e..442fcc396a75 100644 --- a/cmd/loki-canary/main.go +++ b/cmd/loki-canary/main.go @@ -1,11 +1,14 @@ package main import ( + "flag" "fmt" + "io/ioutil" "net/http" "net/url" "os" "os/signal" + "strconv" "time" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -17,22 +20,60 @@ import ( func main() { - c := comparator.NewComparator(os.Stderr, 1*time.Minute, 1*time.Second) + lName := flag.String("labelname", "name", "The label name for this instance of loki-canary to use in the log selector") + lVal := flag.String("labelvalue", "loki-canary", "The unique label value for this instance of loki-canary to use in the log selector") + usePodName := flag.Bool("usepod", false, "If true, loki-canary will read the pod name from /etc/loki-canary/pod_name as the unique label value") + port := flag.Int("port", 3500, "Port which loki-canary should expose metrics") + addr := flag.String("addr", "", "The Loki server URL:Port, e.g. loki:3100") + tls := flag.Bool("tls", false, "Does the loki connection use TLS?") + user := flag.String("user", "", "Loki username") + pass := flag.String("pass", "", "Loki password") - w := writer.NewWriter(os.Stdout, c, 10*time.Millisecond, 1024) + interval := flag.Duration("interval", 1000*time.Millisecond, "Duration between log entries") + size := flag.Int("size", 100, "Size in bytes of each log line") + wait := flag.Duration("wait", 60*time.Second, "Duration to wait for log entries before reporting them lost") + flag.Parse() + + val := *lVal + if *usePodName { + data, err := ioutil.ReadFile("/etc/loki-canary/name") + if err != nil { + panic(err) + } + val = string(data) + } + + if *addr == "" { + panic("Must specify a Loki address with -addr") + } + + var ui *url.Userinfo + if *user != "" { + ui = url.UserPassword(*user, *pass) + } + + scheme := "ws" + if *tls { + scheme = "wss" + } u := url.URL{ - Scheme: "ws", - Host: "loki:3100", + Scheme: scheme, + Host: *addr, + User: ui, Path: "/api/prom/tail", - RawQuery: "query=" + url.QueryEscape("{name=\"loki-canary\",stream=\"stdout\"}"), + RawQuery: "query=" + url.QueryEscape(fmt.Sprintf("{stream=\"stdout\",%v=\"%v\"}", *lName, val)), } + _, _ = fmt.Fprintf(os.Stderr, "Connecting to loki at %v, querying for label '%v' with value '%v'\n", u.String(), *lName, val) + + c := comparator.NewComparator(os.Stderr, *wait, 1*time.Second) + w := writer.NewWriter(os.Stdout, c, *interval, *size) r := reader.NewReader(os.Stderr, c, u, "", "") http.Handle("/metrics", promhttp.Handler()) go func() { - err := http.ListenAndServe(":2112", nil) + err := http.ListenAndServe(":"+strconv.Itoa(*port), nil) if err != nil { panic(err) } diff --git a/go.mod b/go.mod index 4703d635d1b0..8e45d829f9a6 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/golang/protobuf v1.3.1 // indirect github.com/gorilla/websocket v1.4.0 github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 + github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f github.com/prometheus/common v0.3.0 // indirect github.com/stretchr/testify v1.3.0 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 // indirect diff --git a/pkg/comparator/comparator.go b/pkg/comparator/comparator.go index 7038acaee586..c349752af740 100644 --- a/pkg/comparator/comparator.go +++ b/pkg/comparator/comparator.go @@ -16,13 +16,31 @@ const ( ) var ( - outOfOrderEntry = promauto.NewCounter(prometheus.CounterOpts{ - Name: "out_of_order_entry", - Help: "The total number of processed events", + totalEntries = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: "loki_canary", + Name: "total_entries", + Help: "counts log entries written to the file", }) - missingEntry = promauto.NewCounter(prometheus.CounterOpts{ - Name: "missing_entry", - Help: "The total number of processed events", + outOfOrderEntries = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: "loki_canary", + Name: "out_of_order_entries", + Help: "counts log entries received with a timestamp more recent than the others in the queue", + }) + missingEntries = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: "loki_canary", + Name: "missing_entries", + Help: "counts log entries not received within the maxWait duration and is reported as missing", + }) + unexpectedEntries = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: "loki_canary", + Name: "unexpected_entries", + Help: "counts a log entry received which was not expected (e.g. duplicate, received after reported missing)", + }) + responseLatency = promauto.NewHistogram(prometheus.HistogramOpts{ + Namespace: "loki_canary", + Name: "response_latency", + Help: "is how long it takes for log lines to be returned from Loki in seconds.", + Buckets: []float64{0.5, 1, 2.5, 5, 10, 30, 60}, }) ) @@ -60,21 +78,26 @@ func (c *Comparator) EntrySent(time time.Time) { c.entMtx.Lock() defer c.entMtx.Unlock() c.entries = append(c.entries, &time) + totalEntries.Inc() } +// EntryReceived removes the received entry from the buffer if it exists, reports on out of order entries received func (c *Comparator) EntryReceived(ts time.Time) { c.entMtx.Lock() defer c.entMtx.Unlock() // Output index k := 0 + matched := false for i, e := range c.entries { if ts.Equal(*e) { + matched = true // If this isn't the first item in the list we received it out of order if i != 0 { - outOfOrderEntry.Inc() + outOfOrderEntries.Inc() _, _ = fmt.Fprintf(c.w, ErrOutOfOrderEntry, e, c.entries[:i]) } + responseLatency.Observe(time.Now().Sub(ts).Seconds()) // Do not increment output index, effectively causing this element to be dropped } else { // If the current index doesn't match the output index, update the array with the correct position @@ -84,6 +107,9 @@ func (c *Comparator) EntryReceived(ts time.Time) { k++ } } + if !matched { + unexpectedEntries.Inc() + } // Nil out the pointers to any trailing elements which were removed from the slice for i := k; i < len(c.entries); i++ { c.entries[i] = nil // or the zero value of T @@ -120,7 +146,7 @@ func (c *Comparator) pruneEntries() { for i, e := range c.entries { // If the time is outside our range, assume the entry has been lost report and remove it if e.Before(time.Now().Add(-c.maxWait)) { - missingEntry.Inc() + missingEntries.Inc() _, _ = fmt.Fprintf(c.w, ErrEntryNotReceived, e, c.maxWait.Seconds()) } else { if i != k { diff --git a/pkg/comparator/comparator_test.go b/pkg/comparator/comparator_test.go index 37275e8aba50..6e8cc5faad50 100644 --- a/pkg/comparator/comparator_test.go +++ b/pkg/comparator/comparator_test.go @@ -3,13 +3,20 @@ package comparator import ( "bytes" "fmt" + "sync" "testing" "time" + "github.com/prometheus/client_golang/prometheus" + io_prometheus_client "github.com/prometheus/client_model/go" "github.com/stretchr/testify/assert" ) func TestComparatorEntryReceivedOutOfOrder(t *testing.T) { + outOfOrderEntries = &mockCounter{} + missingEntries = &mockCounter{} + unexpectedEntries = &mockCounter{} + actual := &bytes.Buffer{} c := NewComparator(actual, 1*time.Hour, 1*time.Hour) @@ -24,13 +31,26 @@ func TestComparatorEntryReceivedOutOfOrder(t *testing.T) { c.EntrySent(t4) c.EntryReceived(t1) + assert.Equal(t, 3, c.Size()) c.EntryReceived(t4) - expected := fmt.Sprintf(ErrOutOfOrderEntry, t4, []time.Time{t2, t3}) + assert.Equal(t, 2, c.Size()) + c.EntryReceived(t2) + c.EntryReceived(t3) + assert.Equal(t, 0, c.Size()) + expected := fmt.Sprintf(ErrOutOfOrderEntry, t4, []time.Time{t2, t3}) assert.Equal(t, expected, actual.String()) + + assert.Equal(t, 1, outOfOrderEntries.(*mockCounter).count) + assert.Equal(t, 0, unexpectedEntries.(*mockCounter).count) + assert.Equal(t, 0, missingEntries.(*mockCounter).count) } func TestComparatorEntryReceivedNotExpected(t *testing.T) { + outOfOrderEntries = &mockCounter{} + missingEntries = &mockCounter{} + unexpectedEntries = &mockCounter{} + actual := &bytes.Buffer{} c := NewComparator(actual, 1*time.Hour, 1*time.Hour) @@ -51,12 +71,19 @@ func TestComparatorEntryReceivedNotExpected(t *testing.T) { assert.Equal(t, 1, c.Size()) c.EntryReceived(t4) assert.Equal(t, 0, c.Size()) - expected := "" + expected := "" assert.Equal(t, expected, actual.String()) + + assert.Equal(t, 0, outOfOrderEntries.(*mockCounter).count) + assert.Equal(t, 1, unexpectedEntries.(*mockCounter).count) + assert.Equal(t, 0, missingEntries.(*mockCounter).count) } func TestEntryNeverReceived(t *testing.T) { + outOfOrderEntries = &mockCounter{} + missingEntries = &mockCounter{} + unexpectedEntries = &mockCounter{} actual := &bytes.Buffer{} c := NewComparator(actual, 5*time.Millisecond, 2*time.Millisecond) @@ -86,4 +113,39 @@ func TestEntryNeverReceived(t *testing.T) { assert.Equal(t, expected, actual.String()) assert.Equal(t, 0, c.Size()) + assert.Equal(t, 0, outOfOrderEntries.(*mockCounter).count) + assert.Equal(t, 0, unexpectedEntries.(*mockCounter).count) + assert.Equal(t, 1, missingEntries.(*mockCounter).count) + +} + +type mockCounter struct { + cLck sync.Mutex + count int +} + +func (m *mockCounter) Desc() *prometheus.Desc { + panic("implement me") +} + +func (m *mockCounter) Write(*io_prometheus_client.Metric) error { + panic("implement me") +} + +func (m *mockCounter) Describe(chan<- *prometheus.Desc) { + panic("implement me") +} + +func (m *mockCounter) Collect(chan<- prometheus.Metric) { + panic("implement me") +} + +func (m *mockCounter) Add(float64) { + panic("implement me") +} + +func (m *mockCounter) Inc() { + m.cLck.Lock() + defer m.cLck.Unlock() + m.count++ } diff --git a/pkg/reader/logproto.pb.go b/pkg/reader/logproto.pb.go index a555d6d9087a..39c6ac71f2b7 100644 --- a/pkg/reader/logproto.pb.go +++ b/pkg/reader/logproto.pb.go @@ -3,19 +3,20 @@ package reader import ( - context "context" - fmt "fmt" + "context" + "fmt" + "io" + "math" + "reflect" + "strconv" + "strings" + "time" + _ "github.com/gogo/protobuf/gogoproto" - proto "github.com/gogo/protobuf/proto" + "github.com/gogo/protobuf/proto" _ "github.com/gogo/protobuf/types" github_com_gogo_protobuf_types "github.com/gogo/protobuf/types" - grpc "google.golang.org/grpc" - io "io" - math "math" - reflect "reflect" - strconv "strconv" - strings "strings" - time "time" + "google.golang.org/grpc" ) // Reference imports to suppress errors if they are not otherwise used. diff --git a/production/ksonnet/loki-canary/jsonnetfile.json b/production/ksonnet/loki-canary/jsonnetfile.json new file mode 100644 index 000000000000..c903ac17c07c --- /dev/null +++ b/production/ksonnet/loki-canary/jsonnetfile.json @@ -0,0 +1,14 @@ +{ + "dependencies": [ + { + "name": "ksonnet-util", + "source": { + "git": { + "remote": "https://github.com/grafana/jsonnet-libs", + "subdir": "ksonnet-util" + } + }, + "version": "master" + } + ] +} diff --git a/production/ksonnet/loki-canary/loki-canary.libsonnet b/production/ksonnet/loki-canary/loki-canary.libsonnet new file mode 100644 index 000000000000..195df2aaa6e2 --- /dev/null +++ b/production/ksonnet/loki-canary/loki-canary.libsonnet @@ -0,0 +1,46 @@ +local k = import 'ksonnet-util/kausal.libsonnet'; + +k { + local container = $.core.v1.container, + + loki_canary_args:: {}, + + _images+:: { + loki_canary: 'loki-canary:latest', + }, + + loki_canary_container:: + container.new('loki-canary', $._images.loki_canary) + + container.withPorts($.core.v1.containerPort.new('http-metrics', 80)) + + container.withArgsMixin($.util.mapToFlags($.loki_canary_args)) + + container.withEnv([ + container.envType.fromFieldPath('HOSTNAME', 'spec.nodeName'), + ]), + + local daemonSet = $.extensions.v1beta1.daemonSet, + + local downwardApiMount(name, path, volumeMountMixin={}) = + local container = $.core.v1.container, + deployment = $.extensions.v1beta1.deployment, + volumeMount = $.core.v1.volumeMount, + volume = $.core.v1.volume, + addMount(c) = c + container.withVolumeMountsMixin( + volumeMount.new(name, path) + + volumeMountMixin, + ); + + deployment.mapContainers(addMount) + + deployment.mixin.spec.template.spec.withVolumesMixin([ + volume.withName(name) + + volume.mixin.downwardApi.withItems([ + { + path: "name", + fieldRef: { fieldPath: "metadata.name" }, + }, + ]), + ]), + + loki_canary_daemonset: + daemonSet.new('loki-canary', [$.loki_canary_container]) + + downwardApiMount('pod-name', '/etc/loki-canary'), +} \ No newline at end of file From 50ff10dfff98d07c27b8a04a566208b4bb08482e Mon Sep 17 00:00:00 2001 From: Ed Date: Tue, 14 May 2019 11:18:08 -0400 Subject: [PATCH 03/17] Add LICENSE --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000000..261eeb9e9f8b --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From 34bb9db758807a7cf577a865c488a07949867927 Mon Sep 17 00:00:00 2001 From: Edward Welch Date: Tue, 14 May 2019 11:02:40 -0400 Subject: [PATCH 04/17] using an environment variable passed into an arg instead of reading the pod name from a file and the downardApi added a makefile --- .gitignore | 1 + Makefile | 25 +++++++++++++++ cmd/loki-canary/main.go | 18 +++-------- .../ksonnet/loki-canary/loki-canary.libsonnet | 31 ++++--------------- tools/image-tag | 9 ++++++ 5 files changed, 45 insertions(+), 39 deletions(-) create mode 100644 Makefile create mode 100755 tools/image-tag diff --git a/.gitignore b/.gitignore index e69de29bb2d1..eae84e7a0ce9 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1 @@ +loki-canary \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 000000000000..8e2476fb34de --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +.PHONY: all build test clean build-image push-image +.DEFAULT_GOAL := all + +IMAGE_PREFIX ?= grafana +IMAGE_TAG := $(shell ./tools/image-tag) + +all: test build-image + +build: + go build -o loki-canary -v cmd/loki-canary/main.go + +test: + go test -v ./... + +clean: + rm -f ./loki-canary + go clean ./... + +build-image: + docker build -t $(IMAGE_PREFIX)/loki-canary . + docker tag $(IMAGE_PREFIX)/loki-canary $(IMAGE_PREFIX)/loki-canary:$(IMAGE_TAG) + +push-image: + docker push $(IMAGE_PREFIX)/loki-canary:$(IMAGE_TAG) + docker push $(IMAGE_PREFIX)/loki-canary:latest \ No newline at end of file diff --git a/cmd/loki-canary/main.go b/cmd/loki-canary/main.go index 442fcc396a75..dc609427faaf 100644 --- a/cmd/loki-canary/main.go +++ b/cmd/loki-canary/main.go @@ -3,7 +3,6 @@ package main import ( "flag" "fmt" - "io/ioutil" "net/http" "net/url" "os" @@ -22,7 +21,6 @@ func main() { lName := flag.String("labelname", "name", "The label name for this instance of loki-canary to use in the log selector") lVal := flag.String("labelvalue", "loki-canary", "The unique label value for this instance of loki-canary to use in the log selector") - usePodName := flag.Bool("usepod", false, "If true, loki-canary will read the pod name from /etc/loki-canary/pod_name as the unique label value") port := flag.Int("port", 3500, "Port which loki-canary should expose metrics") addr := flag.String("addr", "", "The Loki server URL:Port, e.g. loki:3100") tls := flag.Bool("tls", false, "Does the loki connection use TLS?") @@ -34,17 +32,9 @@ func main() { wait := flag.Duration("wait", 60*time.Second, "Duration to wait for log entries before reporting them lost") flag.Parse() - val := *lVal - if *usePodName { - data, err := ioutil.ReadFile("/etc/loki-canary/name") - if err != nil { - panic(err) - } - val = string(data) - } - if *addr == "" { - panic("Must specify a Loki address with -addr") + _, _ = fmt.Fprintf(os.Stderr, "Must specify a Loki address with -addr\n") + os.Exit(1) } var ui *url.Userinfo @@ -62,10 +52,10 @@ func main() { Host: *addr, User: ui, Path: "/api/prom/tail", - RawQuery: "query=" + url.QueryEscape(fmt.Sprintf("{stream=\"stdout\",%v=\"%v\"}", *lName, val)), + RawQuery: "query=" + url.QueryEscape(fmt.Sprintf("{stream=\"stdout\",%v=\"%v\"}", *lName, *lVal)), } - _, _ = fmt.Fprintf(os.Stderr, "Connecting to loki at %v, querying for label '%v' with value '%v'\n", u.String(), *lName, val) + _, _ = fmt.Fprintf(os.Stderr, "Connecting to loki at %v, querying for label '%v' with value '%v'\n", u.String(), *lName, *lVal) c := comparator.NewComparator(os.Stderr, *wait, 1*time.Second) w := writer.NewWriter(os.Stdout, c, *interval, *size) diff --git a/production/ksonnet/loki-canary/loki-canary.libsonnet b/production/ksonnet/loki-canary/loki-canary.libsonnet index 195df2aaa6e2..b5a6bf65e44d 100644 --- a/production/ksonnet/loki-canary/loki-canary.libsonnet +++ b/production/ksonnet/loki-canary/loki-canary.libsonnet @@ -3,10 +3,12 @@ local k = import 'ksonnet-util/kausal.libsonnet'; k { local container = $.core.v1.container, - loki_canary_args:: {}, + loki_canary_args:: { + labelvalue: "$(POD_NAME)", + }, _images+:: { - loki_canary: 'loki-canary:latest', + loki_canary: 'grafana/loki-canary:latest', }, loki_canary_container:: @@ -15,32 +17,11 @@ k { container.withArgsMixin($.util.mapToFlags($.loki_canary_args)) + container.withEnv([ container.envType.fromFieldPath('HOSTNAME', 'spec.nodeName'), + container.envType.fromFieldPath('POD_NAME', 'metadata.name'), ]), local daemonSet = $.extensions.v1beta1.daemonSet, - local downwardApiMount(name, path, volumeMountMixin={}) = - local container = $.core.v1.container, - deployment = $.extensions.v1beta1.deployment, - volumeMount = $.core.v1.volumeMount, - volume = $.core.v1.volume, - addMount(c) = c + container.withVolumeMountsMixin( - volumeMount.new(name, path) + - volumeMountMixin, - ); - - deployment.mapContainers(addMount) + - deployment.mixin.spec.template.spec.withVolumesMixin([ - volume.withName(name) + - volume.mixin.downwardApi.withItems([ - { - path: "name", - fieldRef: { fieldPath: "metadata.name" }, - }, - ]), - ]), - loki_canary_daemonset: - daemonSet.new('loki-canary', [$.loki_canary_container]) + - downwardApiMount('pod-name', '/etc/loki-canary'), + daemonSet.new('loki-canary', [$.loki_canary_container]), } \ No newline at end of file diff --git a/tools/image-tag b/tools/image-tag new file mode 100755 index 000000000000..31f023dac0e8 --- /dev/null +++ b/tools/image-tag @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +WORKING_SUFFIX=$(if git status --porcelain | grep -qE '^(?:[^?][^ ]|[^ ][^?])\s'; then echo "-WIP"; else echo ""; fi) +BRANCH_PREFIX=$(git rev-parse --abbrev-ref HEAD) +echo "${BRANCH_PREFIX//\//-}-$(git rev-parse --short HEAD)$WORKING_SUFFIX" From 5fe4ccbe1d9f106fa132bc0b4ab0a555a66ba054 Mon Sep 17 00:00:00 2001 From: Edward Welch Date: Tue, 14 May 2019 12:37:07 -0400 Subject: [PATCH 05/17] adding namespace to ksonnet config, moved images into a config file --- .gitignore | 2 +- production/ksonnet/loki-canary/config.libsonnet | 5 +++++ production/ksonnet/loki-canary/loki-canary.libsonnet | 9 ++++----- 3 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 production/ksonnet/loki-canary/config.libsonnet diff --git a/.gitignore b/.gitignore index eae84e7a0ce9..8615ce54e5da 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -loki-canary \ No newline at end of file +./loki-canary \ No newline at end of file diff --git a/production/ksonnet/loki-canary/config.libsonnet b/production/ksonnet/loki-canary/config.libsonnet new file mode 100644 index 000000000000..d1043c450e2d --- /dev/null +++ b/production/ksonnet/loki-canary/config.libsonnet @@ -0,0 +1,5 @@ +{ + _images+:: { + loki_canary: 'grafana/loki-canary:latest', + }, +} \ No newline at end of file diff --git a/production/ksonnet/loki-canary/loki-canary.libsonnet b/production/ksonnet/loki-canary/loki-canary.libsonnet index b5a6bf65e44d..e940a417c157 100644 --- a/production/ksonnet/loki-canary/loki-canary.libsonnet +++ b/production/ksonnet/loki-canary/loki-canary.libsonnet @@ -1,16 +1,15 @@ local k = import 'ksonnet-util/kausal.libsonnet'; +local config = import 'config.libsonnet'; + +k + config { + namespace: $.core.v1.namespace.new($._config.namespace), -k { local container = $.core.v1.container, loki_canary_args:: { labelvalue: "$(POD_NAME)", }, - _images+:: { - loki_canary: 'grafana/loki-canary:latest', - }, - loki_canary_container:: container.new('loki-canary', $._images.loki_canary) + container.withPorts($.core.v1.containerPort.new('http-metrics', 80)) + From 9dde2c4e5f9be43d432b4dd469828a0a3cccbdd9 Mon Sep 17 00:00:00 2001 From: Edward Welch Date: Tue, 14 May 2019 12:57:53 -0400 Subject: [PATCH 06/17] fixing how auth is applied --- cmd/loki-canary/main.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/cmd/loki-canary/main.go b/cmd/loki-canary/main.go index dc609427faaf..002ce4322418 100644 --- a/cmd/loki-canary/main.go +++ b/cmd/loki-canary/main.go @@ -37,11 +37,6 @@ func main() { os.Exit(1) } - var ui *url.Userinfo - if *user != "" { - ui = url.UserPassword(*user, *pass) - } - scheme := "ws" if *tls { scheme = "wss" @@ -50,7 +45,6 @@ func main() { u := url.URL{ Scheme: scheme, Host: *addr, - User: ui, Path: "/api/prom/tail", RawQuery: "query=" + url.QueryEscape(fmt.Sprintf("{stream=\"stdout\",%v=\"%v\"}", *lName, *lVal)), } @@ -59,7 +53,7 @@ func main() { c := comparator.NewComparator(os.Stderr, *wait, 1*time.Second) w := writer.NewWriter(os.Stdout, c, *interval, *size) - r := reader.NewReader(os.Stderr, c, u, "", "") + r := reader.NewReader(os.Stderr, c, u, *user, *pass) http.Handle("/metrics", promhttp.Handler()) go func() { From cc60398bb8c6752b4479e9da59dd2f44221c5d95 Mon Sep 17 00:00:00 2001 From: Edward Welch Date: Tue, 14 May 2019 21:28:33 -0400 Subject: [PATCH 07/17] change response_latency bucket sizes --- pkg/comparator/comparator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/comparator/comparator.go b/pkg/comparator/comparator.go index c349752af740..79a7568d39f7 100644 --- a/pkg/comparator/comparator.go +++ b/pkg/comparator/comparator.go @@ -40,7 +40,7 @@ var ( Namespace: "loki_canary", Name: "response_latency", Help: "is how long it takes for log lines to be returned from Loki in seconds.", - Buckets: []float64{0.5, 1, 2.5, 5, 10, 30, 60}, + Buckets: []float64{1, 5, 10, 30, 60, 90, 120, 300}, }) ) From 5aac00f4d940dc039776c55954d9eedf433dbd21 Mon Sep 17 00:00:00 2001 From: Edward Welch Date: Tue, 14 May 2019 22:17:19 -0400 Subject: [PATCH 08/17] changing response_latency buckets some more, making exponential and configurable --- cmd/loki-canary/main.go | 3 ++- pkg/comparator/comparator.go | 16 +++++++++------- pkg/comparator/comparator_test.go | 21 ++++++++++++++++++--- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/cmd/loki-canary/main.go b/cmd/loki-canary/main.go index 002ce4322418..4315714c6504 100644 --- a/cmd/loki-canary/main.go +++ b/cmd/loki-canary/main.go @@ -30,6 +30,7 @@ func main() { interval := flag.Duration("interval", 1000*time.Millisecond, "Duration between log entries") size := flag.Int("size", 100, "Size in bytes of each log line") wait := flag.Duration("wait", 60*time.Second, "Duration to wait for log entries before reporting them lost") + buckets := flag.Int("buckets", 10, "Number of buckets in the response_latency histogram") flag.Parse() if *addr == "" { @@ -51,7 +52,7 @@ func main() { _, _ = fmt.Fprintf(os.Stderr, "Connecting to loki at %v, querying for label '%v' with value '%v'\n", u.String(), *lName, *lVal) - c := comparator.NewComparator(os.Stderr, *wait, 1*time.Second) + c := comparator.NewComparator(os.Stderr, *wait, 1*time.Second, *buckets) w := writer.NewWriter(os.Stdout, c, *interval, *size) r := reader.NewReader(os.Stderr, c, u, *user, *pass) diff --git a/pkg/comparator/comparator.go b/pkg/comparator/comparator.go index 79a7568d39f7..2e1c34b30bbc 100644 --- a/pkg/comparator/comparator.go +++ b/pkg/comparator/comparator.go @@ -36,12 +36,7 @@ var ( Name: "unexpected_entries", Help: "counts a log entry received which was not expected (e.g. duplicate, received after reported missing)", }) - responseLatency = promauto.NewHistogram(prometheus.HistogramOpts{ - Namespace: "loki_canary", - Name: "response_latency", - Help: "is how long it takes for log lines to be returned from Loki in seconds.", - Buckets: []float64{1, 5, 10, 30, 60, 90, 120, 300}, - }) + responseLatency prometheus.Histogram ) type Comparator struct { @@ -54,7 +49,7 @@ type Comparator struct { done chan struct{} } -func NewComparator(writer io.Writer, maxWait time.Duration, pruneInterval time.Duration) *Comparator { +func NewComparator(writer io.Writer, maxWait time.Duration, pruneInterval time.Duration, buckets int) *Comparator { c := &Comparator{ w: writer, entries: []*time.Time{}, @@ -64,6 +59,13 @@ func NewComparator(writer io.Writer, maxWait time.Duration, pruneInterval time.D done: make(chan struct{}), } + responseLatency = promauto.NewHistogram(prometheus.HistogramOpts{ + Namespace: "loki_canary", + Name: "response_latency", + Help: "is how long it takes for log lines to be returned from Loki in seconds.", + Buckets: prometheus.ExponentialBuckets(0.5, 2, buckets), + }) + go c.run() return c diff --git a/pkg/comparator/comparator_test.go b/pkg/comparator/comparator_test.go index 6e8cc5faad50..f7d7a24cdd12 100644 --- a/pkg/comparator/comparator_test.go +++ b/pkg/comparator/comparator_test.go @@ -18,7 +18,7 @@ func TestComparatorEntryReceivedOutOfOrder(t *testing.T) { unexpectedEntries = &mockCounter{} actual := &bytes.Buffer{} - c := NewComparator(actual, 1*time.Hour, 1*time.Hour) + c := NewComparator(actual, 1*time.Hour, 1*time.Hour, 1) t1 := time.Now() t2 := t1.Add(1 * time.Second) @@ -44,6 +44,11 @@ func TestComparatorEntryReceivedOutOfOrder(t *testing.T) { assert.Equal(t, 1, outOfOrderEntries.(*mockCounter).count) assert.Equal(t, 0, unexpectedEntries.(*mockCounter).count) assert.Equal(t, 0, missingEntries.(*mockCounter).count) + + // This avoids a panic on subsequent test execution, + // seems ugly but was easy, and multiple instantiations + // of the comparator should be an error + prometheus.Unregister(responseLatency) } func TestComparatorEntryReceivedNotExpected(t *testing.T) { @@ -52,7 +57,7 @@ func TestComparatorEntryReceivedNotExpected(t *testing.T) { unexpectedEntries = &mockCounter{} actual := &bytes.Buffer{} - c := NewComparator(actual, 1*time.Hour, 1*time.Hour) + c := NewComparator(actual, 1*time.Hour, 1*time.Hour, 1) t1 := time.Now() t2 := t1.Add(1 * time.Second) @@ -78,6 +83,11 @@ func TestComparatorEntryReceivedNotExpected(t *testing.T) { assert.Equal(t, 0, outOfOrderEntries.(*mockCounter).count) assert.Equal(t, 1, unexpectedEntries.(*mockCounter).count) assert.Equal(t, 0, missingEntries.(*mockCounter).count) + + // This avoids a panic on subsequent test execution, + // seems ugly but was easy, and multiple instantiations + // of the comparator should be an error + prometheus.Unregister(responseLatency) } func TestEntryNeverReceived(t *testing.T) { @@ -86,7 +96,7 @@ func TestEntryNeverReceived(t *testing.T) { unexpectedEntries = &mockCounter{} actual := &bytes.Buffer{} - c := NewComparator(actual, 5*time.Millisecond, 2*time.Millisecond) + c := NewComparator(actual, 5*time.Millisecond, 2*time.Millisecond, 1) t1 := time.Now() t2 := t1.Add(1 * time.Millisecond) @@ -117,6 +127,11 @@ func TestEntryNeverReceived(t *testing.T) { assert.Equal(t, 0, unexpectedEntries.(*mockCounter).count) assert.Equal(t, 1, missingEntries.(*mockCounter).count) + // This avoids a panic on subsequent test execution, + // seems ugly but was easy, and multiple instantiations + // of the comparator should be an error + prometheus.Unregister(responseLatency) + } type mockCounter struct { From d5d4e33d95b639cabc0d7351693544da7a58c618 Mon Sep 17 00:00:00 2001 From: Edward Welch Date: Wed, 15 May 2019 12:54:20 -0400 Subject: [PATCH 09/17] updating README --- README.md | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b24e09dae283..459228b40c1a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,102 @@ +# loki-canary -`docker build -t loki-canary:latest .` +A standalone app to audit the log capturing performance of Loki. -`kubectl run loki-canary --generator=run-pod/v1 --image=loki-canary:latest --restart=Never --image-pull-policy=Never --labels=name=loki-canary` \ No newline at end of file +## how it works + +![block_diagram](docs/block.png) + +loki-canary writes a log to a file and stores the timestamp in an internal array, the contents look something like this: + +```nohighlight +1557935669096040040 ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp +``` + +The relevant part is the timestamp, the `p`'s are just filler bytes to make the size of the log configurable. + +Promtail (or another agent) then reads the log file and ships it to Loki. + +Meanwhile loki-canary opens a websocket connection to loki and listens for logs it creates + +When a log is received on the websocket, the timestamp in the log message is compared to the internal array. + +If the received log is: + + * The next in the array to be received, it is removed from the array and the (current time - log timestamp) is recorded in the `response_latency` histogram, this is the expected behavior for well behaving logs + * Not the next in the array received, is is removed from the array, the response time is recorded in the `response_latency` histogram, and the `out_of_order_entries` counter is incremented + * Not in the array at all, the `unexpected_entries` counter is incremented + +In the background, loki-canary also runs a timer which iterates through all the entries in the internal array, if any are older than the duration specified by the `-wait` flag (default 60s), they are removed from the array and the `missing_entries` counter is incremented + +## building and running + +`make` will run tests and build a docker image + +`make build` will create a binary `loki-canary` alongside the makefile + +To run the image, you can do something simple like: + +`kubectl run loki-canary --generator=run-pod/v1 --image=grafana/loki-canary:latest --restart=Never --image-pull-policy=Never --labels=name=loki-canary` + +Or you can do something more complex like deploy it as a daemonset, there is a ksonnet setup for this in the `production` folder, you can import it using jsonnet-bundler: + +```shell +jb install github.com/grafana/loki-canary/production/ksonnet/loki-canary +``` + +Then in your ksonnet environments `main.jsonnet` you'll want something like this: + +```nohighlight +local loki_canary = import 'loki-canary/loki-canary.libsonnet'; + +loki_canary { + loki_canary_args+:: { + addr: "loki:3100", + port: 80, + labelname: "instance", + interval: "100ms", + size: 1024, + wait: "3m", + }, + _config+:: { + namespace: "default", + } +} + +``` + +## config + +It is required to pass in the Loki address with the `-addr` flag, if your server uses TLS, also pass `-tls=true` (this will create a wss:// instead of ws:// connection) + +You should also pass the `-labelname` and `-labelvalue` flags, these are used by loki-canary to filter the log stream to only process logs for this instance of loki-canary, so they must be unique per each of your loki-canary instances. The ksonnet config in this project accomplishes this by passing in the pod name as the labelvalue + +If you get a high number of `unexpected_entries` you may not be waiting long enough and should increase `-wait` from 60s to something larger. + +All options: + +```nohighlight + -addr string + The Loki server URL:Port, e.g. loki:3100 + -buckets int + Number of buckets in the response_latency histogram (default 10) + -interval duration + Duration between log entries (default 1s) + -labelname string + The label name for this instance of loki-canary to use in the log selector (default "name") + -labelvalue string + The unique label value for this instance of loki-canary to use in the log selector (default "loki-canary") + -pass string + Loki password + -port int + Port which loki-canary should expose metrics (default 3500) + -size int + Size in bytes of each log line (default 100) + -tls + Does the loki connection use TLS? + -user string + Loki username + -wait duration + Duration to wait for log entries before reporting them lost (default 1m0s) +``` \ No newline at end of file From 17419c69cad74ae83ab409210adfa8c8cd482172 Mon Sep 17 00:00:00 2001 From: Edward Welch Date: Wed, 15 May 2019 13:19:17 -0400 Subject: [PATCH 10/17] adding diagram --- docs/block.png | Bin 0 -> 56500 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/block.png diff --git a/docs/block.png b/docs/block.png new file mode 100644 index 0000000000000000000000000000000000000000..f7dd39047bed776a536674e841705e43c3cdea40 GIT binary patch literal 56500 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 Date: Thu, 16 May 2019 07:31:54 -0400 Subject: [PATCH 11/17] updating to match websocket api changes --- go.sum | 3 +++ pkg/reader/reader.go | 35 +++++++++++++++++++++-------------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/go.sum b/go.sum index bad7dc6e3465..c1913c3c7a47 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -38,6 +39,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -109,5 +111,6 @@ google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/reader/reader.go b/pkg/reader/reader.go index 026441436dfd..e8eec1f053b2 100644 --- a/pkg/reader/reader.go +++ b/pkg/reader/reader.go @@ -15,6 +15,12 @@ import ( "github.com/grafana/loki-canary/pkg/comparator" ) +// FIXME this is copied and modified a little from the querier package in Loki to avoid importing Loki which indirectly imports cortex which won't build :( +// TailResponse represents response for tail query +type TailResponse struct { + Streams []*Stream `json:"streams"` +} + type Reader struct { url url.URL header http.Header @@ -66,10 +72,10 @@ func (r *Reader) run() { r.closeAndReconnect() - stream := &Stream{} + tailResponse := &TailResponse{} for { - err := r.conn.ReadJSON(stream) + err := r.conn.ReadJSON(tailResponse) if err != nil { if r.shuttingDown { close(r.done) @@ -79,19 +85,20 @@ func (r *Reader) run() { r.closeAndReconnect() continue } - - for _, entry := range stream.Entries { - sp := strings.Split(entry.Line, " ") - if len(sp) != 2 { - _, _ = fmt.Fprintf(r.w, "received invalid entry: %s\n", entry.Line) - continue - } - ts, err := strconv.ParseInt(sp[0], 10, 64) - if err != nil { - _, _ = fmt.Fprintf(r.w, "failed to parse timestamp: %s\n", sp[0]) - continue + for _, stream := range tailResponse.Streams { + for _, entry := range stream.Entries { + sp := strings.Split(entry.Line, " ") + if len(sp) != 2 { + _, _ = fmt.Fprintf(r.w, "received invalid entry: %s\n", entry.Line) + continue + } + ts, err := strconv.ParseInt(sp[0], 10, 64) + if err != nil { + _, _ = fmt.Fprintf(r.w, "failed to parse timestamp: %s\n", sp[0]) + continue + } + r.cm.EntryReceived(time.Unix(0, ts)) } - r.cm.EntryReceived(time.Unix(0, ts)) } } } From 02aed71a28b0a0ce947a410cf5049e43e10ad252 Mon Sep 17 00:00:00 2001 From: Edward Welch Date: Wed, 12 Jun 2019 15:17:04 -0400 Subject: [PATCH 12/17] refactoring things so that the comparator can use the reader to make a direct query to loki to look for logs not received over the websocket before reporting them as missing --- cmd/loki-canary/main.go | 22 ++--- go.mod | 1 + go.sum | 2 + pkg/comparator/comparator.go | 83 +++++++++++++++-- pkg/comparator/comparator_test.go | 91 ++++++++++++------- pkg/reader/reader.go | 143 ++++++++++++++++++++++++++---- pkg/writer/writer.go | 10 +-- 7 files changed, 269 insertions(+), 83 deletions(-) diff --git a/cmd/loki-canary/main.go b/cmd/loki-canary/main.go index 4315714c6504..2ec01c579eb4 100644 --- a/cmd/loki-canary/main.go +++ b/cmd/loki-canary/main.go @@ -4,7 +4,6 @@ import ( "flag" "fmt" "net/http" - "net/url" "os" "os/signal" "strconv" @@ -38,23 +37,12 @@ func main() { os.Exit(1) } - scheme := "ws" - if *tls { - scheme = "wss" - } - - u := url.URL{ - Scheme: scheme, - Host: *addr, - Path: "/api/prom/tail", - RawQuery: "query=" + url.QueryEscape(fmt.Sprintf("{stream=\"stdout\",%v=\"%v\"}", *lName, *lVal)), - } - - _, _ = fmt.Fprintf(os.Stderr, "Connecting to loki at %v, querying for label '%v' with value '%v'\n", u.String(), *lName, *lVal) + sentChan := make(chan time.Time) + receivedChan := make(chan time.Time) - c := comparator.NewComparator(os.Stderr, *wait, 1*time.Second, *buckets) - w := writer.NewWriter(os.Stdout, c, *interval, *size) - r := reader.NewReader(os.Stderr, c, u, *user, *pass) + w := writer.NewWriter(os.Stdout, sentChan, *interval, *size) + r := reader.NewReader(os.Stderr, receivedChan, *tls, *addr, *user, *pass, *lName, *lVal) + c := comparator.NewComparator(os.Stderr, *wait, 1*time.Second, *buckets, sentChan, receivedChan, r) http.Handle("/metrics", promhttp.Handler()) go func() { diff --git a/go.mod b/go.mod index 8e45d829f9a6..410bfebd8445 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/gogo/protobuf v1.2.1 github.com/golang/protobuf v1.3.1 // indirect github.com/gorilla/websocket v1.4.0 + github.com/pkg/errors v0.8.1 github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f github.com/prometheus/common v0.3.0 // indirect diff --git a/go.sum b/go.sum index c1913c3c7a47..a9fca1518c5b 100644 --- a/go.sum +++ b/go.sum @@ -41,6 +41,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= diff --git a/pkg/comparator/comparator.go b/pkg/comparator/comparator.go index 2e1c34b30bbc..344d15447f5e 100644 --- a/pkg/comparator/comparator.go +++ b/pkg/comparator/comparator.go @@ -8,11 +8,14 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/grafana/loki-canary/pkg/reader" ) const ( - ErrOutOfOrderEntry = "entry %s was received before entries: %v\n" - ErrEntryNotReceived = "failed to receive entry %s within %f seconds\n" + ErrOutOfOrderEntry = "out of order entry %s was received before entries: %v\n" + ErrEntryNotReceivedWs = "websocket failed to receive entry %v within %f seconds\n" + ErrEntryNotReceived = "failed to receive entry %v within %f seconds\n" ) var ( @@ -26,10 +29,15 @@ var ( Name: "out_of_order_entries", Help: "counts log entries received with a timestamp more recent than the others in the queue", }) + wsMissingEntries = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: "loki_canary", + Name: "websocket_missing_entries", + Help: "counts log entries not received within the maxWait duration via the websocket connection", + }) missingEntries = promauto.NewCounter(prometheus.CounterOpts{ Namespace: "loki_canary", Name: "missing_entries", - Help: "counts log entries not received within the maxWait duration and is reported as missing", + Help: "counts log entries not received within the maxWait duration via both websocket and direct query", }) unexpectedEntries = promauto.NewCounter(prometheus.CounterOpts{ Namespace: "loki_canary", @@ -45,16 +53,23 @@ type Comparator struct { entries []*time.Time maxWait time.Duration pruneInterval time.Duration + sent chan time.Time + recv chan time.Time + rdr reader.LokiReader quit chan struct{} done chan struct{} } -func NewComparator(writer io.Writer, maxWait time.Duration, pruneInterval time.Duration, buckets int) *Comparator { +func NewComparator(writer io.Writer, maxWait time.Duration, pruneInterval time.Duration, + buckets int, sentChan chan time.Time, receivedChan chan time.Time, reader reader.LokiReader) *Comparator { c := &Comparator{ w: writer, entries: []*time.Time{}, maxWait: maxWait, pruneInterval: pruneInterval, + sent: sentChan, + recv: receivedChan, + rdr: reader, quit: make(chan struct{}), done: make(chan struct{}), } @@ -76,15 +91,15 @@ func (c *Comparator) Stop() { <-c.done } -func (c *Comparator) EntrySent(time time.Time) { +func (c *Comparator) entrySent(time time.Time) { c.entMtx.Lock() defer c.entMtx.Unlock() c.entries = append(c.entries, &time) totalEntries.Inc() } -// EntryReceived removes the received entry from the buffer if it exists, reports on out of order entries received -func (c *Comparator) EntryReceived(ts time.Time) { +// entryReceived removes the received entry from the buffer if it exists, reports on out of order entries received +func (c *Comparator) entryReceived(ts time.Time) { c.entMtx.Lock() defer c.entMtx.Unlock() @@ -132,6 +147,10 @@ func (c *Comparator) run() { for { select { + case e := <-c.recv: + c.entryReceived(e) + case e := <-c.sent: + c.entrySent(e) case <-t.C: c.pruneEntries() case <-c.quit: @@ -144,12 +163,14 @@ func (c *Comparator) pruneEntries() { c.entMtx.Lock() defer c.entMtx.Unlock() + missing := []*time.Time{} k := 0 for i, e := range c.entries { // If the time is outside our range, assume the entry has been lost report and remove it if e.Before(time.Now().Add(-c.maxWait)) { - missingEntries.Inc() - _, _ = fmt.Fprintf(c.w, ErrEntryNotReceived, e, c.maxWait.Seconds()) + missing = append(missing, e) + wsMissingEntries.Inc() + _, _ = fmt.Fprintf(c.w, ErrEntryNotReceivedWs, e.UnixNano(), c.maxWait.Seconds()) } else { if i != k { c.entries[k] = c.entries[i] @@ -162,4 +183,48 @@ func (c *Comparator) pruneEntries() { c.entries[i] = nil // or the zero value of T } c.entries = c.entries[:k] + if len(missing) > 0 { + go c.confirmMissing(missing) + } +} + +func (c *Comparator) confirmMissing(missing []*time.Time) { + // Because we are querying loki timestamps vs the timestamp in the log, + // make the range +/- 10 seconds to allow for clock inaccuracies + start := *missing[0] + start = start.Add(-10 * time.Second) + end := *missing[len(missing)-1] + end = end.Add(10 * time.Second) + recvd, err := c.rdr.Query(start, end) + if err != nil { + _, _ = fmt.Fprintf(c.w, "error querying loki: %s", err) + return + } + k := 0 + for i, m := range missing { + found := false + for _, r := range recvd { + if (*m).Equal(r) { + // Entry was found in loki, this can be dropped from the list of missing + // which is done by NOT incrementing the output index k + found = true + } + } + if !found { + // Item is still missing + if i != k { + missing[k] = missing[i] + } + k++ + } + } + // Nil out the pointers to any trailing elements which were removed from the slice + for i := k; i < len(missing); i++ { + missing[i] = nil // or the zero value of T + } + missing = missing[:k] + for _, e := range missing { + missingEntries.Inc() + _, _ = fmt.Fprintf(c.w, ErrEntryNotReceived, e.UnixNano(), c.maxWait.Seconds()) + } } diff --git a/pkg/comparator/comparator_test.go b/pkg/comparator/comparator_test.go index f7d7a24cdd12..011aae3c5a4b 100644 --- a/pkg/comparator/comparator_test.go +++ b/pkg/comparator/comparator_test.go @@ -14,28 +14,28 @@ import ( func TestComparatorEntryReceivedOutOfOrder(t *testing.T) { outOfOrderEntries = &mockCounter{} - missingEntries = &mockCounter{} + wsMissingEntries = &mockCounter{} unexpectedEntries = &mockCounter{} actual := &bytes.Buffer{} - c := NewComparator(actual, 1*time.Hour, 1*time.Hour, 1) + c := NewComparator(actual, 1*time.Hour, 1*time.Hour, 1, make(chan time.Time), make(chan time.Time), nil) t1 := time.Now() t2 := t1.Add(1 * time.Second) t3 := t2.Add(1 * time.Second) t4 := t3.Add(1 * time.Second) - c.EntrySent(t1) - c.EntrySent(t2) - c.EntrySent(t3) - c.EntrySent(t4) + c.entrySent(t1) + c.entrySent(t2) + c.entrySent(t3) + c.entrySent(t4) - c.EntryReceived(t1) + c.entryReceived(t1) assert.Equal(t, 3, c.Size()) - c.EntryReceived(t4) + c.entryReceived(t4) assert.Equal(t, 2, c.Size()) - c.EntryReceived(t2) - c.EntryReceived(t3) + c.entryReceived(t2) + c.entryReceived(t3) assert.Equal(t, 0, c.Size()) expected := fmt.Sprintf(ErrOutOfOrderEntry, t4, []time.Time{t2, t3}) @@ -43,7 +43,7 @@ func TestComparatorEntryReceivedOutOfOrder(t *testing.T) { assert.Equal(t, 1, outOfOrderEntries.(*mockCounter).count) assert.Equal(t, 0, unexpectedEntries.(*mockCounter).count) - assert.Equal(t, 0, missingEntries.(*mockCounter).count) + assert.Equal(t, 0, wsMissingEntries.(*mockCounter).count) // This avoids a panic on subsequent test execution, // seems ugly but was easy, and multiple instantiations @@ -53,28 +53,28 @@ func TestComparatorEntryReceivedOutOfOrder(t *testing.T) { func TestComparatorEntryReceivedNotExpected(t *testing.T) { outOfOrderEntries = &mockCounter{} - missingEntries = &mockCounter{} + wsMissingEntries = &mockCounter{} unexpectedEntries = &mockCounter{} actual := &bytes.Buffer{} - c := NewComparator(actual, 1*time.Hour, 1*time.Hour, 1) + c := NewComparator(actual, 1*time.Hour, 1*time.Hour, 1, make(chan time.Time), make(chan time.Time), nil) t1 := time.Now() t2 := t1.Add(1 * time.Second) t3 := t2.Add(1 * time.Second) t4 := t3.Add(1 * time.Second) - c.EntrySent(t2) - c.EntrySent(t3) - c.EntrySent(t4) + c.entrySent(t2) + c.entrySent(t3) + c.entrySent(t4) - c.EntryReceived(t2) + c.entryReceived(t2) assert.Equal(t, 2, c.Size()) - c.EntryReceived(t1) + c.entryReceived(t1) assert.Equal(t, 2, c.Size()) - c.EntryReceived(t3) + c.entryReceived(t3) assert.Equal(t, 1, c.Size()) - c.EntryReceived(t4) + c.entryReceived(t4) assert.Equal(t, 0, c.Size()) expected := "" @@ -82,7 +82,7 @@ func TestComparatorEntryReceivedNotExpected(t *testing.T) { assert.Equal(t, 0, outOfOrderEntries.(*mockCounter).count) assert.Equal(t, 1, unexpectedEntries.(*mockCounter).count) - assert.Equal(t, 0, missingEntries.(*mockCounter).count) + assert.Equal(t, 0, wsMissingEntries.(*mockCounter).count) // This avoids a panic on subsequent test execution, // seems ugly but was easy, and multiple instantiations @@ -92,39 +92,54 @@ func TestComparatorEntryReceivedNotExpected(t *testing.T) { func TestEntryNeverReceived(t *testing.T) { outOfOrderEntries = &mockCounter{} + wsMissingEntries = &mockCounter{} missingEntries = &mockCounter{} unexpectedEntries = &mockCounter{} actual := &bytes.Buffer{} - c := NewComparator(actual, 5*time.Millisecond, 2*time.Millisecond, 1) t1 := time.Now() t2 := t1.Add(1 * time.Millisecond) t3 := t2.Add(1 * time.Millisecond) t4 := t3.Add(1 * time.Millisecond) + t5 := t4.Add(1 * time.Millisecond) - c.EntrySent(t1) - c.EntrySent(t2) - c.EntrySent(t3) - c.EntrySent(t4) + found := []time.Time{t1, t3, t4, t5} - assert.Equal(t, 4, c.Size()) + mr := &mockReader{found} + maxWait := 5 * time.Millisecond + c := NewComparator(actual, maxWait, 2*time.Millisecond, 1, make(chan time.Time), make(chan time.Time), mr) - c.EntryReceived(t1) - c.EntryReceived(t2) - c.EntryReceived(t3) + c.entrySent(t1) + c.entrySent(t2) + c.entrySent(t3) + c.entrySent(t4) + c.entrySent(t5) - assert.Equal(t, 1, c.Size()) + assert.Equal(t, 5, c.Size()) + + c.entryReceived(t1) + c.entryReceived(t3) + c.entryReceived(t5) + + assert.Equal(t, 2, c.Size()) - <-time.After(10 * time.Millisecond) + //Wait a few maxWait intervals just to make sure all entries are expired and the async confirmMissing has completed + <-time.After(2 * maxWait) - expected := fmt.Sprintf(ErrEntryNotReceived, t4, 5*time.Millisecond.Seconds()) + expected := fmt.Sprintf(ErrOutOfOrderEntry+ErrOutOfOrderEntry+ErrEntryNotReceivedWs+ErrEntryNotReceived+ErrEntryNotReceivedWs, + t3, []time.Time{t2}, + t5, []time.Time{t2, t4}, + t2.UnixNano(), maxWait.Seconds(), + t2.UnixNano(), maxWait.Seconds(), + t4.UnixNano(), maxWait.Seconds()) assert.Equal(t, expected, actual.String()) assert.Equal(t, 0, c.Size()) - assert.Equal(t, 0, outOfOrderEntries.(*mockCounter).count) + assert.Equal(t, 2, outOfOrderEntries.(*mockCounter).count) assert.Equal(t, 0, unexpectedEntries.(*mockCounter).count) + assert.Equal(t, 2, wsMissingEntries.(*mockCounter).count) assert.Equal(t, 1, missingEntries.(*mockCounter).count) // This avoids a panic on subsequent test execution, @@ -164,3 +179,11 @@ func (m *mockCounter) Inc() { defer m.cLck.Unlock() m.count++ } + +type mockReader struct { + resp []time.Time +} + +func (r *mockReader) Query(start time.Time, end time.Time) ([]time.Time, error) { + return r.resp, nil +} diff --git a/pkg/reader/reader.go b/pkg/reader/reader.go index e8eec1f053b2..41ac31c81782 100644 --- a/pkg/reader/reader.go +++ b/pkg/reader/reader.go @@ -2,8 +2,11 @@ package reader import ( "encoding/base64" + "encoding/json" "fmt" "io" + "io/ioutil" + "log" "net/http" "net/url" "strconv" @@ -11,8 +14,17 @@ import ( "time" "github.com/gorilla/websocket" + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) - "github.com/grafana/loki-canary/pkg/comparator" +var ( + reconnects = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: "loki_canary", + Name: "ws_reconnects", + Help: "counts every time the websocket connection has to reconnect", + }) ) // FIXME this is copied and modified a little from the querier package in Loki to avoid importing Loki which indirectly imports cortex which won't build :( @@ -21,27 +33,43 @@ type TailResponse struct { Streams []*Stream `json:"streams"` } +type LokiReader interface { + Query(start time.Time, end time.Time) ([]time.Time, error) +} + type Reader struct { - url url.URL header http.Header + tls bool + addr string + user string + pass string + lName string + lVal string conn *websocket.Conn w io.Writer - cm *comparator.Comparator + recv chan time.Time quit chan struct{} shuttingDown bool done chan struct{} } -func NewReader(writer io.Writer, comparator *comparator.Comparator, url url.URL, user string, pass string) *Reader { +func NewReader(writer io.Writer, receivedChan chan time.Time, tls bool, + address string, user string, pass string, labelName string, labelVal string) *Reader { h := http.Header{} if user != "" { h = http.Header{"Authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+pass))}} } + rd := Reader{ - w: writer, - url: url, - cm: comparator, header: h, + tls: tls, + addr: address, + user: user, + pass: pass, + lName: labelName, + lVal: labelVal, + w: writer, + recv: receivedChan, quit: make(chan struct{}), done: make(chan struct{}), shuttingDown: false, @@ -68,6 +96,63 @@ func (r *Reader) Stop() { <-r.done } +func (r *Reader) Query(start time.Time, end time.Time) ([]time.Time, error) { + scheme := "http" + if r.tls { + scheme = "https" + } + u := url.URL{ + Scheme: scheme, + Host: r.addr, + Path: "/api/prom/query", + RawQuery: fmt.Sprintf("start=%d&end=%d", start.UnixNano(), end.UnixNano()) + "&query=" + + url.QueryEscape(fmt.Sprintf("{stream=\"stdout\",%v=\"%v\"}", r.lName, r.lVal)), + } + _, _ = fmt.Fprintf(r.w, "Querying loki for missing values with query: %v\n", u.String()) + + client := &http.Client{} + + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, err + } + + req.SetBasicAuth(r.user, r.pass) + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer func() { + if err := resp.Body.Close(); err != nil { + log.Println("error closing body", err) + } + }() + + if resp.StatusCode/100 != 2 { + buf, _ := ioutil.ReadAll(resp.Body) + return nil, fmt.Errorf("error response from server: %s (%v)", string(buf), err) + } + var decoded QueryResponse + err = json.NewDecoder(resp.Body).Decode(&decoded) + + tss := []time.Time{} + + for _, stream := range decoded.Streams { + for _, entry := range stream.Entries { + ts, err := parseResponse(&entry) + if err != nil { + _, _ = fmt.Fprint(r.w, err) + continue + } + tss = append(tss, *ts) + } + + } + + return tss, nil +} + func (r *Reader) run() { r.closeAndReconnect() @@ -87,17 +172,12 @@ func (r *Reader) run() { } for _, stream := range tailResponse.Streams { for _, entry := range stream.Entries { - sp := strings.Split(entry.Line, " ") - if len(sp) != 2 { - _, _ = fmt.Fprintf(r.w, "received invalid entry: %s\n", entry.Line) - continue - } - ts, err := strconv.ParseInt(sp[0], 10, 64) + ts, err := parseResponse(&entry) if err != nil { - _, _ = fmt.Fprintf(r.w, "failed to parse timestamp: %s\n", sp[0]) + _, _ = fmt.Fprint(r.w, err) continue } - r.cm.EntryReceived(time.Unix(0, ts)) + r.recv <- *ts } } } @@ -107,14 +187,43 @@ func (r *Reader) closeAndReconnect() { if r.conn != nil { _ = r.conn.Close() r.conn = nil + // By incrementing reconnects here we should only count a failure followed by a successful reconnect. + // Initial connections and reconnections from failed tries will not be counted. + reconnects.Inc() } for r.conn == nil { - c, _, err := websocket.DefaultDialer.Dial(r.url.String(), r.header) + scheme := "ws" + if r.tls { + scheme = "wss" + } + u := url.URL{ + Scheme: scheme, + Host: r.addr, + Path: "/api/prom/tail", + RawQuery: "query=" + url.QueryEscape(fmt.Sprintf("{stream=\"stdout\",%v=\"%v\"}", r.lName, r.lVal)), + } + + _, _ = fmt.Fprintf(r.w, "Connecting to loki at %v, querying for label '%v' with value '%v'\n", u.String(), r.lName, r.lVal) + + c, _, err := websocket.DefaultDialer.Dial(u.String(), r.header) if err != nil { - _, _ = fmt.Fprintf(r.w, "failed to connect to %s with err %s\n", r.url.String(), err) + _, _ = fmt.Fprintf(r.w, "failed to connect to %s with err %s\n", u.String(), err) <-time.After(5 * time.Second) continue } r.conn = c } } + +func parseResponse(entry *Entry) (*time.Time, error) { + sp := strings.Split(entry.Line, " ") + if len(sp) != 2 { + return nil, errors.Errorf("received invalid entry: %s\n", entry.Line) + } + ts, err := strconv.ParseInt(sp[0], 10, 64) + if err != nil { + return nil, errors.Errorf("failed to parse timestamp: %s\n", sp[0]) + } + t := time.Unix(0, ts) + return &t, nil +} diff --git a/pkg/writer/writer.go b/pkg/writer/writer.go index addb28e420d4..74024f7c094a 100644 --- a/pkg/writer/writer.go +++ b/pkg/writer/writer.go @@ -6,8 +6,6 @@ import ( "strconv" "strings" "time" - - "github.com/grafana/loki-canary/pkg/comparator" ) const ( @@ -16,7 +14,7 @@ const ( type Writer struct { w io.Writer - cm *comparator.Comparator + sent chan time.Time interval time.Duration size int prevTsLen int @@ -25,11 +23,11 @@ type Writer struct { done chan struct{} } -func NewWriter(writer io.Writer, comparator *comparator.Comparator, entryInterval time.Duration, entrySize int) *Writer { +func NewWriter(writer io.Writer, sentChan chan time.Time, entryInterval time.Duration, entrySize int) *Writer { w := &Writer{ w: writer, - cm: comparator, + sent: sentChan, interval: entryInterval, size: entrySize, prevTsLen: 0, @@ -72,7 +70,7 @@ func (w *Writer) run() { } _, _ = fmt.Fprintf(w.w, LogEntry, ts, w.pad) - w.cm.EntrySent(t) + w.sent <- t case <-w.quit: return } From 7cf05c5dfcde9983633883ab58bdd3bdd73c7263 Mon Sep 17 00:00:00 2001 From: Edward Welch Date: Thu, 13 Jun 2019 09:29:31 -0400 Subject: [PATCH 13/17] adding a list of received entries (acknowledged) to use for comparison against entries which were not expected so that we can report them as duplicates. --- pkg/comparator/comparator.go | 44 ++++++++++++++- pkg/comparator/comparator_test.go | 89 ++++++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/pkg/comparator/comparator.go b/pkg/comparator/comparator.go index 344d15447f5e..e78398e3199b 100644 --- a/pkg/comparator/comparator.go +++ b/pkg/comparator/comparator.go @@ -16,6 +16,8 @@ const ( ErrOutOfOrderEntry = "out of order entry %s was received before entries: %v\n" ErrEntryNotReceivedWs = "websocket failed to receive entry %v within %f seconds\n" ErrEntryNotReceived = "failed to receive entry %v within %f seconds\n" + ErrDuplicateEntry = "received a duplicate entry for ts %v\n" + ErrUnexpectedEntry = "received an unexpected entry with ts %v\n" ) var ( @@ -42,7 +44,12 @@ var ( unexpectedEntries = promauto.NewCounter(prometheus.CounterOpts{ Namespace: "loki_canary", Name: "unexpected_entries", - Help: "counts a log entry received which was not expected (e.g. duplicate, received after reported missing)", + Help: "counts a log entry received which was not expected (e.g. received after reported missing)", + }) + duplicateEntries = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: "loki_canary", + Name: "duplicate_entries", + Help: "counts a log entry received more than one time", }) responseLatency prometheus.Histogram ) @@ -51,6 +58,7 @@ type Comparator struct { entMtx sync.Mutex w io.Writer entries []*time.Time + ackdEntries []*time.Time maxWait time.Duration pruneInterval time.Duration sent chan time.Time @@ -115,6 +123,8 @@ func (c *Comparator) entryReceived(ts time.Time) { _, _ = fmt.Fprintf(c.w, ErrOutOfOrderEntry, e, c.entries[:i]) } responseLatency.Observe(time.Now().Sub(ts).Seconds()) + // Put this element in the acknowledged entries list so we can use it to check for duplicates + c.ackdEntries = append(c.ackdEntries, c.entries[i]) // Do not increment output index, effectively causing this element to be dropped } else { // If the current index doesn't match the output index, update the array with the correct position @@ -125,7 +135,19 @@ func (c *Comparator) entryReceived(ts time.Time) { } } if !matched { - unexpectedEntries.Inc() + duplicate := false + for _, e := range c.ackdEntries { + if ts.Equal(*e) { + duplicate = true + duplicateEntries.Inc() + _, _ = fmt.Fprintf(c.w, ErrDuplicateEntry, ts.UnixNano()) + break + } + } + if !duplicate { + _, _ = fmt.Fprintf(c.w, ErrUnexpectedEntry, ts.UnixNano()) + unexpectedEntries.Inc() + } } // Nil out the pointers to any trailing elements which were removed from the slice for i := k; i < len(c.entries); i++ { @@ -186,6 +208,24 @@ func (c *Comparator) pruneEntries() { if len(missing) > 0 { go c.confirmMissing(missing) } + + // Prune the acknowledged list, remove anything older than our maxwait + k = 0 + for i, e := range c.ackdEntries { + if e.Before(time.Now().Add(-c.maxWait)) { + // Do nothing, if we don't increment the output index k, this will be dropped + } else { + if i != k { + c.ackdEntries[k] = c.ackdEntries[i] + } + k++ + } + } + // Nil out the pointers to any trailing elements which were removed from the slice + for i := k; i < len(c.ackdEntries); i++ { + c.ackdEntries[i] = nil // or the zero value of T + } + c.ackdEntries = c.ackdEntries[:k] } func (c *Comparator) confirmMissing(missing []*time.Time) { diff --git a/pkg/comparator/comparator_test.go b/pkg/comparator/comparator_test.go index 011aae3c5a4b..452f39b2114c 100644 --- a/pkg/comparator/comparator_test.go +++ b/pkg/comparator/comparator_test.go @@ -16,6 +16,7 @@ func TestComparatorEntryReceivedOutOfOrder(t *testing.T) { outOfOrderEntries = &mockCounter{} wsMissingEntries = &mockCounter{} unexpectedEntries = &mockCounter{} + duplicateEntries = &mockCounter{} actual := &bytes.Buffer{} c := NewComparator(actual, 1*time.Hour, 1*time.Hour, 1, make(chan time.Time), make(chan time.Time), nil) @@ -44,6 +45,7 @@ func TestComparatorEntryReceivedOutOfOrder(t *testing.T) { assert.Equal(t, 1, outOfOrderEntries.(*mockCounter).count) assert.Equal(t, 0, unexpectedEntries.(*mockCounter).count) assert.Equal(t, 0, wsMissingEntries.(*mockCounter).count) + assert.Equal(t, 0, duplicateEntries.(*mockCounter).count) // This avoids a panic on subsequent test execution, // seems ugly but was easy, and multiple instantiations @@ -55,6 +57,7 @@ func TestComparatorEntryReceivedNotExpected(t *testing.T) { outOfOrderEntries = &mockCounter{} wsMissingEntries = &mockCounter{} unexpectedEntries = &mockCounter{} + duplicateEntries = &mockCounter{} actual := &bytes.Buffer{} c := NewComparator(actual, 1*time.Hour, 1*time.Hour, 1, make(chan time.Time), make(chan time.Time), nil) @@ -77,12 +80,57 @@ func TestComparatorEntryReceivedNotExpected(t *testing.T) { c.entryReceived(t4) assert.Equal(t, 0, c.Size()) - expected := "" + expected := fmt.Sprintf(ErrUnexpectedEntry, t1.UnixNano()) assert.Equal(t, expected, actual.String()) assert.Equal(t, 0, outOfOrderEntries.(*mockCounter).count) assert.Equal(t, 1, unexpectedEntries.(*mockCounter).count) assert.Equal(t, 0, wsMissingEntries.(*mockCounter).count) + assert.Equal(t, 0, duplicateEntries.(*mockCounter).count) + + // This avoids a panic on subsequent test execution, + // seems ugly but was easy, and multiple instantiations + // of the comparator should be an error + prometheus.Unregister(responseLatency) +} + +func TestComparatorEntryReceivedDuplicate(t *testing.T) { + outOfOrderEntries = &mockCounter{} + wsMissingEntries = &mockCounter{} + unexpectedEntries = &mockCounter{} + duplicateEntries = &mockCounter{} + + actual := &bytes.Buffer{} + c := NewComparator(actual, 1*time.Hour, 1*time.Hour, 1, make(chan time.Time), make(chan time.Time), nil) + + t1 := time.Now() + t2 := t1.Add(1 * time.Second) + t3 := t2.Add(1 * time.Second) + t4 := t3.Add(1 * time.Second) + + c.entrySent(t1) + c.entrySent(t2) + c.entrySent(t3) + c.entrySent(t4) + + c.entryReceived(t1) + assert.Equal(t, 3, c.Size()) + c.entryReceived(t2) + assert.Equal(t, 2, c.Size()) + c.entryReceived(t2) + assert.Equal(t, 2, c.Size()) + c.entryReceived(t3) + assert.Equal(t, 1, c.Size()) + c.entryReceived(t4) + assert.Equal(t, 0, c.Size()) + + expected := fmt.Sprintf(ErrDuplicateEntry, t2.UnixNano()) + assert.Equal(t, expected, actual.String()) + + assert.Equal(t, 0, outOfOrderEntries.(*mockCounter).count) + assert.Equal(t, 0, unexpectedEntries.(*mockCounter).count) + assert.Equal(t, 0, wsMissingEntries.(*mockCounter).count) + assert.Equal(t, 1, duplicateEntries.(*mockCounter).count) // This avoids a panic on subsequent test execution, // seems ugly but was easy, and multiple instantiations @@ -95,6 +143,7 @@ func TestEntryNeverReceived(t *testing.T) { wsMissingEntries = &mockCounter{} missingEntries = &mockCounter{} unexpectedEntries = &mockCounter{} + duplicateEntries = &mockCounter{} actual := &bytes.Buffer{} @@ -141,6 +190,7 @@ func TestEntryNeverReceived(t *testing.T) { assert.Equal(t, 0, unexpectedEntries.(*mockCounter).count) assert.Equal(t, 2, wsMissingEntries.(*mockCounter).count) assert.Equal(t, 1, missingEntries.(*mockCounter).count) + assert.Equal(t, 0, duplicateEntries.(*mockCounter).count) // This avoids a panic on subsequent test execution, // seems ugly but was easy, and multiple instantiations @@ -149,6 +199,43 @@ func TestEntryNeverReceived(t *testing.T) { } +func TestPruneAckdEntires(t *testing.T) { + actual := &bytes.Buffer{} + maxWait := 30 * time.Millisecond + c := NewComparator(actual, maxWait, 10*time.Millisecond, 1, make(chan time.Time), make(chan time.Time), nil) + + t1 := time.Now() + t2 := t1.Add(1 * time.Millisecond) + t3 := t2.Add(1 * time.Millisecond) + t4 := t3.Add(100 * time.Millisecond) + + assert.Equal(t, 0, len(c.ackdEntries)) + + c.entrySent(t1) + c.entrySent(t2) + c.entrySent(t3) + c.entrySent(t4) + + assert.Equal(t, 4, c.Size()) + assert.Equal(t, 0, len(c.ackdEntries)) + + c.entryReceived(t1) + c.entryReceived(t2) + c.entryReceived(t3) + c.entryReceived(t4) + + assert.Equal(t, 0, c.Size()) + assert.Equal(t, 4, len(c.ackdEntries)) + + // Wait a couple maxWaits to make sure the first 3 timestamps get pruned from the ackdEntries, + // the fourth should still remain because it was 100ms newer than t3 + <-time.After(2 * maxWait) + + assert.Equal(t, 1, len(c.ackdEntries)) + assert.Equal(t, t4, *c.ackdEntries[0]) + +} + type mockCounter struct { cLck sync.Mutex count int From 4bfcd09d6b82cbebdb3f2e419b4e6bcbb5627320 Mon Sep 17 00:00:00 2001 From: Edward Welch Date: Mon, 24 Jun 2019 14:09:46 -0400 Subject: [PATCH 14/17] adding resource requests in jsonnet --- production/ksonnet/loki-canary/loki-canary.libsonnet | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/production/ksonnet/loki-canary/loki-canary.libsonnet b/production/ksonnet/loki-canary/loki-canary.libsonnet index e940a417c157..a07c920f6d2a 100644 --- a/production/ksonnet/loki-canary/loki-canary.libsonnet +++ b/production/ksonnet/loki-canary/loki-canary.libsonnet @@ -12,6 +12,7 @@ k + config { loki_canary_container:: container.new('loki-canary', $._images.loki_canary) + + $.util.resourcesRequests('10m', '20Mi') + container.withPorts($.core.v1.containerPort.new('http-metrics', 80)) + container.withArgsMixin($.util.mapToFlags($.loki_canary_args)) + container.withEnv([ @@ -23,4 +24,4 @@ k + config { loki_canary_daemonset: daemonSet.new('loki-canary', [$.loki_canary_container]), -} \ No newline at end of file +} From bcccbbfaaf0f0e703581295b72ac66154519f4c3 Mon Sep 17 00:00:00 2001 From: Edward Welch Date: Wed, 26 Jun 2019 16:49:39 -0400 Subject: [PATCH 15/17] changing the default prune interval to 60 seconds, at the previous 1 second it would cause some way too aggressive querying when scaled out over all our clusters and loki had a hiccup --- cmd/loki-canary/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/loki-canary/main.go b/cmd/loki-canary/main.go index 2ec01c579eb4..820cedaff3e0 100644 --- a/cmd/loki-canary/main.go +++ b/cmd/loki-canary/main.go @@ -42,7 +42,7 @@ func main() { w := writer.NewWriter(os.Stdout, sentChan, *interval, *size) r := reader.NewReader(os.Stderr, receivedChan, *tls, *addr, *user, *pass, *lName, *lVal) - c := comparator.NewComparator(os.Stderr, *wait, 1*time.Second, *buckets, sentChan, receivedChan, r) + c := comparator.NewComparator(os.Stderr, *wait, 60*time.Second, *buckets, sentChan, receivedChan, r) http.Handle("/metrics", promhttp.Handler()) go func() { From eedd3d9fc2cb6f47aae879722ebf0df6d118eabb Mon Sep 17 00:00:00 2001 From: Edward Welch Date: Tue, 9 Jul 2019 14:58:06 -0400 Subject: [PATCH 16/17] prune interval is configurable canary will suspend all operations on SIGINT but not exit, allowing you to shutdown the canary without it being restarted by docker/kubernetes SIGTERM will shutdown everything and end the process --- README.md | 12 +++++++++--- cmd/loki-canary/main.go | 11 ++++++++++- pkg/comparator/comparator.go | 7 +++++-- pkg/reader/reader.go | 12 ++++++++---- pkg/writer/writer.go | 7 +++++-- 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 459228b40c1a..60678b472697 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,9 @@ If the received log is: * The next in the array to be received, it is removed from the array and the (current time - log timestamp) is recorded in the `response_latency` histogram, this is the expected behavior for well behaving logs * Not the next in the array received, is is removed from the array, the response time is recorded in the `response_latency` histogram, and the `out_of_order_entries` counter is incremented - * Not in the array at all, the `unexpected_entries` counter is incremented + * Not in the array at all, it is checked against a separate list of received logs to either increment the `duplicate_entries` counter or the `unexpected_entries` counter. -In the background, loki-canary also runs a timer which iterates through all the entries in the internal array, if any are older than the duration specified by the `-wait` flag (default 60s), they are removed from the array and the `missing_entries` counter is incremented +In the background, loki-canary also runs a timer which iterates through all the entries in the internal array, if any are older than the duration specified by the `-wait` flag (default 60s), they are removed from the array and the `websocket_missing_entries` counter is incremented. Then an additional query is made directly to loki for these missing entries to determine if they were actually missing or just didn't make it down the websocket. If they are not found in the followup query the `missing_entries` counter is incremented. ## building and running @@ -74,6 +74,10 @@ You should also pass the `-labelname` and `-labelvalue` flags, these are used by If you get a high number of `unexpected_entries` you may not be waiting long enough and should increase `-wait` from 60s to something larger. +__Be cognizant__ of the relationship between `pruneinterval` and the `interval`. For example, with an interval of 10ms (100 logs per second) and a prune interval of 60s, you will write 6000 logs per minute, if those logs were not received over the websocket, the canary will attempt to query loki directly to see if they are completely lost. __However__ the query return is limited to 1000 results so you will not be able to return all the logs even if they did make it to Loki. + +__Likewise__, if you lower the `pruneinterval` you risk causing a denial of service attack as all your canaries attempt to query for missing logs at whatever your `pruneinterval` is defined at. + All options: ```nohighlight @@ -91,6 +95,8 @@ All options: Loki password -port int Port which loki-canary should expose metrics (default 3500) + -pruneinterval duration + Frequency to check sent vs received logs, also the frequency which queries for missing logs will be dispatched to loki (default 1m0s) -size int Size in bytes of each log line (default 100) -tls @@ -99,4 +105,4 @@ All options: Loki username -wait duration Duration to wait for log entries before reporting them lost (default 1m0s) -``` \ No newline at end of file +``` diff --git a/cmd/loki-canary/main.go b/cmd/loki-canary/main.go index 820cedaff3e0..442656e6d180 100644 --- a/cmd/loki-canary/main.go +++ b/cmd/loki-canary/main.go @@ -7,6 +7,7 @@ import ( "os" "os/signal" "strconv" + "syscall" "time" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -29,6 +30,7 @@ func main() { interval := flag.Duration("interval", 1000*time.Millisecond, "Duration between log entries") size := flag.Int("size", 100, "Size in bytes of each log line") wait := flag.Duration("wait", 60*time.Second, "Duration to wait for log entries before reporting them lost") + pruneInterval := flag.Duration("pruneinterval", 60*time.Second, "Frequency to check sent vs received logs, also the frequency which queries for missing logs will be dispatched to loki") buckets := flag.Int("buckets", 10, "Number of buckets in the response_latency histogram") flag.Parse() @@ -42,7 +44,7 @@ func main() { w := writer.NewWriter(os.Stdout, sentChan, *interval, *size) r := reader.NewReader(os.Stderr, receivedChan, *tls, *addr, *user, *pass, *lName, *lVal) - c := comparator.NewComparator(os.Stderr, *wait, 60*time.Second, *buckets, sentChan, receivedChan, r) + c := comparator.NewComparator(os.Stderr, *wait, *pruneInterval, *buckets, sentChan, receivedChan, r) http.Handle("/metrics", promhttp.Handler()) go func() { @@ -53,11 +55,18 @@ func main() { }() interrupt := make(chan os.Signal, 1) + terminate := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) + signal.Notify(terminate, syscall.SIGTERM) for { select { case <-interrupt: + _, _ = fmt.Fprintf(os.Stderr, "suspending indefinetely\n") + w.Stop() + r.Stop() + c.Stop() + case <-terminate: _, _ = fmt.Fprintf(os.Stderr, "shutting down\n") w.Stop() r.Stop() diff --git a/pkg/comparator/comparator.go b/pkg/comparator/comparator.go index e78398e3199b..dd2451c0c66b 100644 --- a/pkg/comparator/comparator.go +++ b/pkg/comparator/comparator.go @@ -95,8 +95,11 @@ func NewComparator(writer io.Writer, maxWait time.Duration, pruneInterval time.D } func (c *Comparator) Stop() { - close(c.quit) - <-c.done + if c.quit != nil { + close(c.quit) + <-c.done + c.quit = nil + } } func (c *Comparator) entrySent(time time.Time) { diff --git a/pkg/reader/reader.go b/pkg/reader/reader.go index 41ac31c81782..dc80b2a1217f 100644 --- a/pkg/reader/reader.go +++ b/pkg/reader/reader.go @@ -92,8 +92,11 @@ func NewReader(writer io.Writer, receivedChan chan time.Time, tls bool, } func (r *Reader) Stop() { - close(r.quit) - <-r.done + if r.quit != nil { + close(r.quit) + <-r.done + r.quit = nil + } } func (r *Reader) Query(start time.Time, end time.Time) ([]time.Time, error) { @@ -105,8 +108,9 @@ func (r *Reader) Query(start time.Time, end time.Time) ([]time.Time, error) { Scheme: scheme, Host: r.addr, Path: "/api/prom/query", - RawQuery: fmt.Sprintf("start=%d&end=%d", start.UnixNano(), end.UnixNano()) + "&query=" + - url.QueryEscape(fmt.Sprintf("{stream=\"stdout\",%v=\"%v\"}", r.lName, r.lVal)), + RawQuery: fmt.Sprintf("start=%d&end=%d", start.UnixNano(), end.UnixNano()) + + "&query=" + url.QueryEscape(fmt.Sprintf("{stream=\"stdout\",%v=\"%v\"}", r.lName, r.lVal)) + + "&limit=1000", } _, _ = fmt.Fprintf(r.w, "Querying loki for missing values with query: %v\n", u.String()) diff --git a/pkg/writer/writer.go b/pkg/writer/writer.go index 74024f7c094a..8d9b163a808b 100644 --- a/pkg/writer/writer.go +++ b/pkg/writer/writer.go @@ -41,8 +41,11 @@ func NewWriter(writer io.Writer, sentChan chan time.Time, entryInterval time.Dur } func (w *Writer) Stop() { - close(w.quit) - <-w.done + if w.quit != nil { + close(w.quit) + <-w.done + w.quit = nil + } } func (w *Writer) run() { From cd5368a44d53a91ef4f08ae389460f9130a3f1c8 Mon Sep 17 00:00:00 2001 From: Edward Welch Date: Wed, 17 Jul 2019 15:37:58 -0400 Subject: [PATCH 17/17] preparing for move into loki repo --- .gitignore | 1 - Dockerfile | 9 - LICENSE | 201 ------------------ Makefile | 25 --- cmd/loki-canary/Dockerfile | 4 + cmd/loki-canary/main.go | 6 +- README.md => docs/canary/README.md | 2 +- docs/{ => canary}/block.png | Bin go.mod | 19 -- go.sum | 118 ---------- pkg/{ => canary}/comparator/comparator.go | 2 +- .../comparator/comparator_test.go | 0 pkg/{ => canary}/reader/logproto.pb.go | 0 pkg/{ => canary}/reader/reader.go | 0 pkg/{ => canary}/writer/writer.go | 0 tools/image-tag | 9 - 16 files changed, 9 insertions(+), 387 deletions(-) delete mode 100644 .gitignore delete mode 100644 Dockerfile delete mode 100644 LICENSE delete mode 100644 Makefile create mode 100644 cmd/loki-canary/Dockerfile rename README.md => docs/canary/README.md (99%) rename docs/{ => canary}/block.png (100%) delete mode 100644 go.mod delete mode 100644 go.sum rename pkg/{ => canary}/comparator/comparator.go (99%) rename pkg/{ => canary}/comparator/comparator_test.go (100%) rename pkg/{ => canary}/reader/logproto.pb.go (100%) rename pkg/{ => canary}/reader/reader.go (100%) rename pkg/{ => canary}/writer/writer.go (100%) delete mode 100755 tools/image-tag diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 8615ce54e5da..000000000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -./loki-canary \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 068429b86c57..000000000000 --- a/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM golang -COPY ./ /src/ -RUN cd /src && \ - CGO_ENABLED=0 go build -o loki-canary cmd/loki-canary/main.go - -FROM alpine:3.9 -RUN apk add --update --no-cache ca-certificates -COPY --from=0 /src/loki-canary /bin/loki-canary -ENTRYPOINT [ "/bin/loki-canary" ] \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 261eeb9e9f8b..000000000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/Makefile b/Makefile deleted file mode 100644 index 8e2476fb34de..000000000000 --- a/Makefile +++ /dev/null @@ -1,25 +0,0 @@ -.PHONY: all build test clean build-image push-image -.DEFAULT_GOAL := all - -IMAGE_PREFIX ?= grafana -IMAGE_TAG := $(shell ./tools/image-tag) - -all: test build-image - -build: - go build -o loki-canary -v cmd/loki-canary/main.go - -test: - go test -v ./... - -clean: - rm -f ./loki-canary - go clean ./... - -build-image: - docker build -t $(IMAGE_PREFIX)/loki-canary . - docker tag $(IMAGE_PREFIX)/loki-canary $(IMAGE_PREFIX)/loki-canary:$(IMAGE_TAG) - -push-image: - docker push $(IMAGE_PREFIX)/loki-canary:$(IMAGE_TAG) - docker push $(IMAGE_PREFIX)/loki-canary:latest \ No newline at end of file diff --git a/cmd/loki-canary/Dockerfile b/cmd/loki-canary/Dockerfile new file mode 100644 index 000000000000..a8c211d48f55 --- /dev/null +++ b/cmd/loki-canary/Dockerfile @@ -0,0 +1,4 @@ +FROM alpine:3.9 +RUN apk add --update --no-cache ca-certificates +ADD loki-canary /usr/bin +ENTRYPOINT [ "/bin/loki-canary" ] diff --git a/cmd/loki-canary/main.go b/cmd/loki-canary/main.go index 442656e6d180..c9969323fb19 100644 --- a/cmd/loki-canary/main.go +++ b/cmd/loki-canary/main.go @@ -12,9 +12,9 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/grafana/loki-canary/pkg/comparator" - "github.com/grafana/loki-canary/pkg/reader" - "github.com/grafana/loki-canary/pkg/writer" + "github.com/grafana/loki-canary/pkg/canary/comparator" + "github.com/grafana/loki-canary/pkg/canary/reader" + "github.com/grafana/loki-canary/pkg/canary/writer" ) func main() { diff --git a/README.md b/docs/canary/README.md similarity index 99% rename from README.md rename to docs/canary/README.md index 60678b472697..1fc40787ac67 100644 --- a/README.md +++ b/docs/canary/README.md @@ -5,7 +5,7 @@ A standalone app to audit the log capturing performance of Loki. ## how it works -![block_diagram](docs/block.png) +![block_diagram](block.png) loki-canary writes a log to a file and stores the timestamp in an internal array, the contents look something like this: diff --git a/docs/block.png b/docs/canary/block.png similarity index 100% rename from docs/block.png rename to docs/canary/block.png diff --git a/go.mod b/go.mod deleted file mode 100644 index 410bfebd8445..000000000000 --- a/go.mod +++ /dev/null @@ -1,19 +0,0 @@ -module github.com/grafana/loki-canary - -go 1.12 - -require ( - github.com/gogo/protobuf v1.2.1 - github.com/golang/protobuf v1.3.1 // indirect - github.com/gorilla/websocket v1.4.0 - github.com/pkg/errors v0.8.1 - github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 - github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f - github.com/prometheus/common v0.3.0 // indirect - github.com/stretchr/testify v1.3.0 - golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 // indirect - golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 // indirect - google.golang.org/appengine v1.4.0 // indirect - google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 // indirect - google.golang.org/grpc v1.20.1 -) diff --git a/go.sum b/go.sum deleted file mode 100644 index a9fca1518c5b..000000000000 --- a/go.sum +++ /dev/null @@ -1,118 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 h1:D+CiwcpGTW6pL6bv6KI3KbyEyCKyS+1JWS2h8PNDnGA= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f h1:BVwpUVJDADN2ufcGik7W992pyps0wZ888b/y9GXcLTU= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.3.0 h1:taZ4h8Tkxv2kNyoSctBvfXEHmBmxrwmIidZTIaHons4= -github.com/prometheus/common v0.3.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1 h1:/K3IL0Z1quvmJ7X0A1AwNEK7CRkVK3YwfOU/QAL4WGg= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -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= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/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= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/comparator/comparator.go b/pkg/canary/comparator/comparator.go similarity index 99% rename from pkg/comparator/comparator.go rename to pkg/canary/comparator/comparator.go index dd2451c0c66b..1f3bd3792a3d 100644 --- a/pkg/comparator/comparator.go +++ b/pkg/canary/comparator/comparator.go @@ -9,7 +9,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/grafana/loki-canary/pkg/reader" + "github.com/grafana/loki-canary/pkg/canary/reader" ) const ( diff --git a/pkg/comparator/comparator_test.go b/pkg/canary/comparator/comparator_test.go similarity index 100% rename from pkg/comparator/comparator_test.go rename to pkg/canary/comparator/comparator_test.go diff --git a/pkg/reader/logproto.pb.go b/pkg/canary/reader/logproto.pb.go similarity index 100% rename from pkg/reader/logproto.pb.go rename to pkg/canary/reader/logproto.pb.go diff --git a/pkg/reader/reader.go b/pkg/canary/reader/reader.go similarity index 100% rename from pkg/reader/reader.go rename to pkg/canary/reader/reader.go diff --git a/pkg/writer/writer.go b/pkg/canary/writer/writer.go similarity index 100% rename from pkg/writer/writer.go rename to pkg/canary/writer/writer.go diff --git a/tools/image-tag b/tools/image-tag deleted file mode 100755 index 31f023dac0e8..000000000000 --- a/tools/image-tag +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -set -o errexit -set -o nounset -set -o pipefail - -WORKING_SUFFIX=$(if git status --porcelain | grep -qE '^(?:[^?][^ ]|[^ ][^?])\s'; then echo "-WIP"; else echo ""; fi) -BRANCH_PREFIX=$(git rev-parse --abbrev-ref HEAD) -echo "${BRANCH_PREFIX//\//-}-$(git rev-parse --short HEAD)$WORKING_SUFFIX"