├── .gitignore ├── internal ├── pkg │ ├── sheets_test.go │ ├── queue.go │ └── sheets.go └── app │ ├── middleware.go │ └── server.go ├── docs ├── trigger.png ├── dashboard.png ├── slack-alert.png ├── trigger-menu.png └── trigger-menu-2.png ├── Dockerfile ├── go.mod ├── .github └── workflows │ └── docker.yaml ├── cmd └── main.go ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | -------------------------------------------------------------------------------- /internal/pkg/sheets_test.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | -------------------------------------------------------------------------------- /docs/trigger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byrnedo/prometheus-gsheet/HEAD/docs/trigger.png -------------------------------------------------------------------------------- /docs/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byrnedo/prometheus-gsheet/HEAD/docs/dashboard.png -------------------------------------------------------------------------------- /docs/slack-alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byrnedo/prometheus-gsheet/HEAD/docs/slack-alert.png -------------------------------------------------------------------------------- /docs/trigger-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byrnedo/prometheus-gsheet/HEAD/docs/trigger-menu.png -------------------------------------------------------------------------------- /docs/trigger-menu-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byrnedo/prometheus-gsheet/HEAD/docs/trigger-menu-2.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine as builder 2 | WORKDIR /server 3 | ENV CGO_ENABLED 0 4 | ENV GOOS linux 5 | 6 | RUN apk update && apk add --no-cache git ca-certificates upx && update-ca-certificates 7 | 8 | COPY . . 9 | RUN --mount=type=cache,target=/root/.cache/go-build \ 10 | go build -ldflags="-w -s" -o binary ./cmd #&& upx --brute binary 11 | 12 | # Runtime 13 | FROM scratch 14 | WORKDIR / 15 | ARG TZ 16 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 17 | COPY --from=builder /server/binary / 18 | COPY --from=builder /usr/local/go/lib/time/zoneinfo.zip / 19 | ENV TZ=${TZ:-Europe/Stockholm} 20 | ENV ZONEINFO=/zoneinfo.zip 21 | 22 | ENTRYPOINT ["./binary"] 23 | -------------------------------------------------------------------------------- /internal/app/middleware.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/justinas/alice" 5 | "github.com/prometheus/client_golang/prometheus" 6 | "github.com/prometheus/client_golang/prometheus/promauto" 7 | "net/http" 8 | "strconv" 9 | ) 10 | 11 | type StatusRecordingWriter struct { 12 | status int 13 | http.ResponseWriter 14 | } 15 | 16 | func (w *StatusRecordingWriter) WriteHeader(statusCode int) { 17 | w.status = statusCode 18 | w.ResponseWriter.WriteHeader(statusCode) 19 | } 20 | 21 | func metricsMiddleware() alice.Constructor { 22 | 23 | httpResponsesMetric := promauto.NewCounterVec(prometheus.CounterOpts{ 24 | Name: "gsheet_remote_write_http_responses_total", 25 | Help: "Total number of Gsheet Remote Write HTTP responses", 26 | }, []string{"response_code"}) 27 | 28 | totalRequestsMetric := promauto.NewCounter(prometheus.CounterOpts{ 29 | Name: "gsheet_remote_write_received_requests", 30 | Help: "The total number of received requests from Prometheus server", 31 | }) 32 | 33 | return func(next http.Handler) http.Handler { 34 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 35 | totalRequestsMetric.Inc() 36 | rw := &StatusRecordingWriter{ResponseWriter: w} 37 | next.ServeHTTP(rw, r) 38 | httpResponsesMetric.With(prometheus.Labels{"response_code": strconv.Itoa(rw.status)}).Inc() 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/byrnedo/prometheus-gsheet 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/fatih/color v1.15.0 7 | github.com/gogo/protobuf v1.3.2 8 | github.com/golang/snappy v0.0.4 9 | github.com/justinas/alice v1.2.0 10 | github.com/prometheus/client_golang v1.15.1 11 | github.com/prometheus/common v0.42.0 12 | github.com/prometheus/prometheus v0.44.0 13 | github.com/rs/zerolog v1.29.1 14 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29 15 | golang.org/x/oauth2 v0.8.0 16 | golang.org/x/time v0.3.0 17 | google.golang.org/api v0.123.0 18 | ) 19 | 20 | require ( 21 | cloud.google.com/go/compute v1.19.0 // indirect 22 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 23 | github.com/beorn7/perks v1.0.1 // indirect 24 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 25 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 26 | github.com/golang/protobuf v1.5.3 // indirect 27 | github.com/google/s2a-go v0.1.3 // indirect 28 | github.com/google/uuid v1.3.0 // indirect 29 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect 30 | github.com/googleapis/gax-go/v2 v2.8.0 // indirect 31 | github.com/julienschmidt/httprouter v1.3.0 // indirect 32 | github.com/mattn/go-colorable v0.1.13 // indirect 33 | github.com/mattn/go-isatty v0.0.17 // indirect 34 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 35 | github.com/prometheus/client_model v0.3.0 // indirect 36 | github.com/prometheus/procfs v0.9.0 // indirect 37 | go.opencensus.io v0.24.0 // indirect 38 | golang.org/x/crypto v0.7.0 // indirect 39 | golang.org/x/net v0.10.0 // indirect 40 | golang.org/x/sys v0.8.0 // indirect 41 | golang.org/x/text v0.9.0 // indirect 42 | google.golang.org/appengine v1.6.7 // indirect 43 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 44 | google.golang.org/grpc v1.54.0 // indirect 45 | google.golang.org/protobuf v1.30.0 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | # Publish semver tags as releases. 7 | tags: [ 'v*.*.*' ] 8 | pull_request: 9 | branches: [ main ] 10 | 11 | env: 12 | # github.repository as / 13 | IMAGE_NAME: byrnedo/prometheus-gsheet 14 | 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: read 21 | packages: write 22 | # This is used to complete the identity challenge 23 | # with sigstore/fulcio when running outside of PRs. 24 | id-token: write 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v3 29 | 30 | # Workaround: https://github.com/docker/build-push-action/issues/461 31 | - name: Setup Docker buildx 32 | uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf 33 | 34 | # Extract metadata (tags, labels) for Docker 35 | # https://github.com/docker/metadata-action 36 | - name: Extract Docker metadata 37 | id: meta 38 | uses: docker/metadata-action@v4 39 | with: 40 | images: ${{ env.IMAGE_NAME }} 41 | tags: | 42 | type=ref,event=branch 43 | type=semver,pattern={{version}} 44 | 45 | # Login against a Docker registry except on PR 46 | # https://github.com/docker/login-action 47 | - name: Log into registry ${{ env.REGISTRY }} 48 | if: github.event_name != 'pull_request' 49 | uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c 50 | with: 51 | username: byrnedo 52 | password: ${{ secrets.HUB_TOKEN }} 53 | 54 | # Build and push Docker image with Buildx (don't push on PR) 55 | # https://github.com/docker/build-push-action 56 | - name: Build and push 57 | id: build-and-push 58 | uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a 59 | with: 60 | context: . 61 | platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7 62 | push: ${{ github.event_name != 'pull_request' }} 63 | tags: ${{ steps.meta.outputs.tags }} 64 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/byrnedo/prometheus-gsheet/internal/app" 7 | "github.com/byrnedo/prometheus-gsheet/internal/pkg" 8 | "github.com/prometheus/common/model" 9 | "github.com/rs/zerolog" 10 | "github.com/rs/zerolog/log" 11 | "net/http" 12 | _ "net/http/pprof" 13 | "os" 14 | "os/signal" 15 | "syscall" 16 | "time" 17 | ) 18 | 19 | func envDefault(name string, deflt string) string { 20 | if v, found := os.LookupEnv(name); !found { 21 | return deflt 22 | } else { 23 | return v 24 | } 25 | } 26 | func mustEnv(name string) string { 27 | if v, found := os.LookupEnv(name); !found { 28 | panic("Missing environment variable: " + name) 29 | } else { 30 | return v 31 | } 32 | } 33 | 34 | var ( 35 | cAddr = envDefault("ADDR", ":4700") 36 | cLogFormat = envDefault("LOG_FORMAT", "console") 37 | cCredentials = mustEnv("CREDENTIALS") 38 | cSpreadsheetID = mustEnv("SPREADSHEET_ID") 39 | ) 40 | 41 | func init() { 42 | if cLogFormat == "console" { 43 | log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339Nano}).With().Timestamp().Logger() 44 | } 45 | } 46 | 47 | func main() { 48 | 49 | done := make(chan int, 1) 50 | go func() { 51 | sigChan := make(chan os.Signal, 1) 52 | signal.Notify(sigChan, syscall.SIGINT) 53 | sig := <-sigChan 54 | 55 | log.Info().Msgf("received %s", sig) 56 | done <- 1 57 | }() 58 | 59 | sheetsSvc := pkg.NewClient(cSpreadsheetID, 0) 60 | 61 | if err := sheetsSvc.Authenticate(context.Background(), cCredentials); err != nil { 62 | panic(fmt.Sprintf("CREDENTIALS: %s", err)) 63 | } 64 | 65 | // create server struct 66 | server := app.Server{Addr: cAddr, Queue: &pkg.Queue{ 67 | BufferSize: 500, 68 | Client: sheetsSvc, 69 | RequestTimeout: 120 * time.Second, 70 | Chan: make(chan *model.Sample, 500), 71 | }} 72 | go func() { 73 | _ = http.ListenAndServe(":6060", nil) 74 | }() 75 | 76 | // listen 77 | go func() { 78 | if err := server.ListenAndServe(); err != nil { 79 | if err != http.ErrServerClosed { 80 | log.Err(err).Msgf("server error: %w", err) 81 | } 82 | done <- 2 83 | return 84 | } 85 | done <- 0 86 | }() 87 | 88 | exitCode := <-done 89 | log.Info().Msg("shutting down") 90 | // cleanup 91 | if err := server.Close(); err != nil { 92 | log.Fatal().Err(err).Msgf("failed to close server cleanly: %w", err) 93 | } 94 | 95 | os.Exit(exitCode) 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prometheus Gsheet 2 | 3 | Remote write to google sheets 4 | 5 | ![img.png](docs/dashboard.png) 6 | 7 | Turns out to work pretty well for a homelab setup. 8 | 9 | Docker images available for armv6,armv7,arm64 and amd64 as [byrnedo/prometheus-gsheet](https://hub.docker.com/repository/docker/byrnedo/prometheus-gsheet) 10 | 11 | ## Config 12 | 13 | - LISTEN - the address to listen to, defaults to :4700 14 | - CREDENTIALS - **required**: base64 credentials.json for google api 15 | - SPREADSHEET_ID - **required**: the hash id from the google sheet url (i.e. the last bit 16 | from `https://docs.google.com/spreadsheets/d/1Wm55yV6TUN74Yjw4ButU52AUUUzA6Xkh2rfN2t0YD3Y`) 17 | 18 | On the prometheus side, you need to add something like the following to `prometheus.yml`: 19 | 20 | ```yaml 21 | remote_write: 22 | - name: ghseets 23 | url: http://gsheets:4700/ 24 | ``` 25 | 26 | See [the prometheus docs](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_write) for more config 27 | options. 28 | 29 | ## Sheet columns 30 | 31 | Are, in order from left to right: 32 | 33 | - Consumed timestamp 34 | - Metric timestamp 35 | - Metric name 36 | - Metric value 37 | - Metric dimensions 38 | 39 | ## Example Google Sheet 40 | 41 | You can copy the following google sheet and use it as a starting point. 42 | [MetricsTemplate](https://docs.google.com/spreadsheets/d/1G07scg4xKhn8-PzlRXCxLgvTu729ayTWfZqK0MpDFRk/edit?usp=sharing) 43 | 44 | Prometheus-gsheet will push metrics to the first sheet of the supplied document. 45 | 46 | ## Alerting 47 | 48 | The template sheet includes alerting via slack. 49 | You'll need to supply a slack webhook url in order for that to work (in the `Config` sheet) 50 | 51 | ![img.png](docs/slack-alert.png) 52 | 53 | Also, the `buildReport` function in the apps scripts needs a trigger (in this case every 5 minutes): 54 | 55 | ![img.png](docs/trigger-menu.png) 56 | 57 | ![img.png](docs/trigger-menu-2.png) 58 | 59 | ![img.png](docs/trigger.png) 60 | 61 | ## Custom Sheet Functions 62 | 63 | In order to be able to graph and alert on things like cpu, the following sheet functions are available via appsscript: 64 | 65 | - TS_GROUPBY - allows grouping by a dimension 66 | - TS_RATE - sort of like `rate` in prometheus, useful for calculating cpu usage 67 | - TS_DATETIME - maps first column in result from unix timestamp to a date time 68 | - TS_AVERAGE - averages over the timeseries 69 | 70 | Most of these functions work with or produce results where the first column is a unix timestamp. 71 | 72 | **Note**: 73 | These functions are a bit of a hack to say the least. YMMV. 74 | 75 | ## Roadmap 76 | 77 | - [ ] Rework apps script 78 | - [ ] More example graphs 79 | - [ ] Alert via arbitrary url 80 | - [ ] Fix TS_RATE function, doesn't seem to handle rollover among other things. -------------------------------------------------------------------------------- /internal/app/server.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/byrnedo/prometheus-gsheet/internal/pkg" 5 | "github.com/fatih/color" 6 | "github.com/gogo/protobuf/proto" 7 | "github.com/julienschmidt/httprouter" 8 | "github.com/prometheus/client_golang/prometheus" 9 | "github.com/prometheus/common/model" 10 | "github.com/rs/zerolog/log" 11 | "io/ioutil" 12 | "net/http" 13 | "time" 14 | 15 | "github.com/golang/snappy" 16 | "github.com/justinas/alice" 17 | "github.com/prometheus/prometheus/prompb" 18 | ) 19 | 20 | var bluePrint = color.New(color.FgBlue).SprintfFunc() 21 | 22 | type Server struct { 23 | Addr string 24 | srv *http.Server 25 | totalRequestsMetric prometheus.Counter 26 | httpResponsesMetric *prometheus.CounterVec 27 | Queue *pkg.Queue 28 | } 29 | 30 | func (s *Server) ListenAndServe() error { 31 | 32 | rtr := s.router() 33 | 34 | s.srv = &http.Server{ 35 | Addr: s.Addr, 36 | Handler: rtr, 37 | } 38 | 39 | go func() { 40 | if err := s.Queue.ListenAndProcess(); err != nil { 41 | log.Err(err) 42 | } 43 | }() 44 | 45 | log.Info().Msgf("server listening on %s", s.srv.Addr) 46 | 47 | return s.srv.ListenAndServe() 48 | } 49 | 50 | func (s *Server) protoToSamples(req *prompb.WriteRequest) model.Samples { 51 | var samples model.Samples 52 | for _, ts := range req.Timeseries { 53 | metric := make(model.Metric, len(ts.Labels)) 54 | for _, l := range ts.Labels { 55 | metric[model.LabelName(l.Name)] = model.LabelValue(l.Value) 56 | } 57 | 58 | for _, s := range ts.Samples { 59 | mt := model.Time(s.Timestamp) 60 | if time.Since(mt.Time()) > 5*time.Minute { 61 | // skipping since too old 62 | continue 63 | } 64 | samples = append(samples, &model.Sample{ 65 | Metric: metric, 66 | Value: model.SampleValue(s.Value), 67 | Timestamp: mt, 68 | }) 69 | } 70 | } 71 | return samples 72 | } 73 | 74 | func (s *Server) handle(w http.ResponseWriter, r *http.Request) { 75 | 76 | compressed, err := ioutil.ReadAll(r.Body) 77 | if err != nil { 78 | http.Error(w, err.Error(), http.StatusInternalServerError) 79 | return 80 | } 81 | 82 | reqBuf, err := snappy.Decode(nil, compressed) 83 | if err != nil { 84 | http.Error(w, err.Error(), http.StatusBadRequest) 85 | return 86 | } 87 | 88 | var req prompb.WriteRequest 89 | if err := proto.Unmarshal(reqBuf, &req); err != nil { 90 | http.Error(w, err.Error(), http.StatusBadRequest) 91 | return 92 | } 93 | 94 | samples := s.protoToSamples(&req) 95 | 96 | s.Queue.Put(samples) 97 | 98 | log.Info().Msgf(bluePrint("received %d samples", len(samples))) 99 | 100 | return 101 | } 102 | 103 | func (s *Server) router() http.Handler { 104 | 105 | m := httprouter.New() 106 | m.Handler("POST", "/", alice.New(metricsMiddleware()).ThenFunc(s.handle)) 107 | return m 108 | } 109 | 110 | func (s Server) Close() error { 111 | //if s.Queue != nil { 112 | // s.Queue.Close() 113 | //} 114 | if s.srv != nil { 115 | return s.srv.Close() 116 | } 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /internal/pkg/queue.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "context" 5 | "github.com/fatih/color" 6 | "github.com/prometheus/common/model" 7 | "github.com/rs/zerolog/log" 8 | "golang.org/x/exp/slices" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | var ( 14 | redPrint = color.New(color.FgRed).SprintfFunc() 15 | greenPrint = color.New(color.FgGreen).SprintfFunc() 16 | ) 17 | 18 | type Queue struct { 19 | BufferSize int 20 | Chan chan *model.Sample 21 | Client *Client 22 | RequestTimeout time.Duration 23 | sheetLimitReached bool 24 | } 25 | 26 | func (q Queue) Put(samples model.Samples) { 27 | for _, s := range samples { 28 | q.Chan <- s 29 | } 30 | } 31 | 32 | func eternalRetry(op func() error, sleep time.Duration, attempt int) { 33 | if err := op(); err != nil { 34 | time.Sleep(sleep) 35 | log.Info().Msgf("retry attempt #%d", attempt+1) 36 | eternalRetry(op, sleep, attempt+1) 37 | } 38 | } 39 | 40 | func (q Queue) getConfig(ctx context.Context) (*Config, error) { 41 | if ctx == nil { 42 | ctx = context.Background() 43 | } 44 | ctx, cncl := context.WithTimeout(context.Background(), 10*time.Second) 45 | defer cncl() 46 | 47 | return q.Client.GetConfig(ctx) 48 | } 49 | 50 | func (q Queue) retireOldMetrics() (int, error) { 51 | 52 | ctx, cncl := context.WithTimeout(context.Background(), 30*time.Second) 53 | defer cncl() 54 | return q.Client.RetireMetrics(ctx) 55 | } 56 | 57 | func (q Queue) ListenAndProcess() error { 58 | 59 | buf := make([]*model.Sample, 0, q.BufferSize) 60 | 61 | config, err := q.getConfig(nil) 62 | if err != nil { 63 | return err 64 | } 65 | retired, err := q.retireOldMetrics() 66 | if err != nil { 67 | return err 68 | } 69 | log.Info().Msgf(redPrint("retired %d metrics", retired)) 70 | 71 | flustTimerDur := 5 * time.Second 72 | flushTimer := time.NewTimer(flustTimerDur) 73 | defer flushTimer.Stop() 74 | 75 | confTicker := time.NewTicker(1 * time.Minute) 76 | defer confTicker.Stop() 77 | 78 | cleanupTicker := time.NewTicker(20 * time.Second) 79 | defer cleanupTicker.Stop() 80 | 81 | flush := func() { 82 | if len(buf) == 0 { 83 | return 84 | } 85 | // send to google 86 | eternalRetry(func() error { 87 | log.Info().Msgf(greenPrint("sending request of %d", len(buf))) 88 | ctx, cncl := context.WithTimeout(context.Background(), q.RequestTimeout) 89 | defer cncl() 90 | 91 | if err := q.Client.Write(ctx, buf, q.sheetLimitReached); err != nil { 92 | if strings.Contains(err.Error(), "This action would increase the number of cells in the workbook above the limit of 10000000 cells.") { 93 | q.sheetLimitReached = true 94 | } 95 | 96 | log.Err(err).Msgf("failed to send to google sheets: %s", err) 97 | return err 98 | } 99 | log.Info().Msgf(greenPrint("successfully sent")) 100 | 101 | buf = make([]*model.Sample, 0, q.BufferSize) 102 | return nil 103 | }, 2*time.Second, 0) 104 | } 105 | 106 | for { 107 | select { 108 | case s := <-q.Chan: 109 | 110 | metricName := strings.ToLower(string(s.Metric[model.MetricNameLabel])) 111 | if !slices.Contains(config.Metrics, metricName) { 112 | continue 113 | } 114 | 115 | buf = append(buf, s) 116 | 117 | if len(buf) == q.BufferSize { 118 | log.Debug().Msgf("buffer filled") 119 | // send 120 | flush() 121 | 122 | if !flushTimer.Stop() { 123 | <-flushTimer.C 124 | } 125 | flushTimer.Reset(flustTimerDur) 126 | } 127 | 128 | case <-flushTimer.C: 129 | log.Debug().Msgf("flush timer triggered") 130 | flush() 131 | flushTimer.Stop() 132 | flushTimer.Reset(flustTimerDur) 133 | case <-confTicker.C: 134 | log.Debug().Msgf("fetching config") 135 | if updConf, err := q.getConfig(nil); err != nil { 136 | log.Err(err).Msgf("failed to get config: %s", err) 137 | } else { 138 | config = updConf 139 | } 140 | case <-cleanupTicker.C: 141 | if retired, err := q.retireOldMetrics(); err != nil { 142 | log.Err(err).Msgf("error trying to retire metrics: %s", err) 143 | continue 144 | } else { 145 | log.Info().Msgf(redPrint("retired %d metrics", retired)) 146 | } 147 | 148 | } 149 | 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /internal/pkg/sheets.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | "github.com/prometheus/common/model" 8 | "github.com/rs/zerolog/log" 9 | "golang.org/x/oauth2/google" 10 | "golang.org/x/time/rate" 11 | "google.golang.org/api/option" 12 | "google.golang.org/api/sheets/v4" 13 | "math" 14 | "sort" 15 | "strconv" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | type Client struct { 21 | svc *sheets.Service 22 | SpreadsheetID string 23 | SheetID int 24 | rateLimiter *rate.Limiter 25 | } 26 | 27 | func NewClient(spreadsheetID string, sheetID int) *Client { 28 | return &Client{ 29 | SpreadsheetID: spreadsheetID, 30 | SheetID: sheetID, 31 | rateLimiter: rate.NewLimiter(rate.Every(1*time.Second), 60), 32 | } 33 | } 34 | 35 | func (c *Client) Authenticate(ctx context.Context, base64Key string) error { 36 | credBytes, err := base64.StdEncoding.DecodeString(base64Key) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | // authenticate and get configuration 42 | config, err := google.JWTConfigFromJSON(credBytes, "https://www.googleapis.com/auth/spreadsheets") 43 | if err != nil { 44 | return err 45 | } 46 | 47 | client := config.Client(ctx) 48 | 49 | c.svc, err = sheets.NewService(ctx, option.WithHTTPClient(client)) 50 | if err != nil { 51 | return err 52 | } 53 | return nil 54 | } 55 | 56 | func ref[T any](v T) *T { 57 | return &v 58 | } 59 | 60 | func (c *Client) Write(ctx context.Context, samples model.Samples, makeRoom bool) error { 61 | 62 | rows := make([]*sheets.RowData, len(samples)) 63 | 64 | now := time.Now() 65 | 66 | for i, s := range samples { 67 | 68 | values := sampleToCells(now, s) 69 | 70 | rows[i] = &sheets.RowData{ 71 | Values: values, 72 | } 73 | } 74 | 75 | req := &sheets.BatchUpdateSpreadsheetRequest{ 76 | IncludeSpreadsheetInResponse: false, 77 | } 78 | 79 | if makeRoom { 80 | req.Requests = append(req.Requests, &sheets.Request{ 81 | DeleteDimension: &sheets.DeleteDimensionRequest{ 82 | Range: &sheets.DimensionRange{ 83 | Dimension: "ROWS", 84 | StartIndex: 0, 85 | EndIndex: int64(len(rows)), 86 | SheetId: int64(c.SheetID), 87 | }, 88 | }, 89 | }) 90 | } 91 | 92 | req.Requests = append(req.Requests, &sheets.Request{ 93 | AppendCells: &sheets.AppendCellsRequest{ 94 | Fields: "*", 95 | Rows: rows, 96 | SheetId: int64(c.SheetID), 97 | }, 98 | }) 99 | 100 | if err := c.rateLimiter.Wait(ctx); err != nil { 101 | return err 102 | } 103 | call := c.svc.Spreadsheets.BatchUpdate(c.SpreadsheetID, req) 104 | _, err := call.Context(ctx).Do() 105 | return err 106 | } 107 | func sampleToCells(now time.Time, s *model.Sample) (cells []*sheets.CellData) { 108 | 109 | cells = append(cells, &sheets.CellData{ 110 | UserEnteredValue: &sheets.ExtendedValue{ 111 | NumberValue: ref(float64(now.UTC().Unix())), 112 | }, 113 | }) 114 | 115 | cells = append(cells, &sheets.CellData{ 116 | UserEnteredValue: &sheets.ExtendedValue{ 117 | NumberValue: ref(float64(s.Timestamp.Time().UTC().Unix())), 118 | }, 119 | }) 120 | 121 | name, _ := s.Metric[model.MetricNameLabel] 122 | 123 | cells = append(cells, &sheets.CellData{ 124 | UserEnteredValue: &sheets.ExtendedValue{ 125 | StringValue: ref(string(name)), 126 | }, 127 | }) 128 | 129 | valueFloat := ref(float64(s.Value)) 130 | if math.IsNaN(*valueFloat) { 131 | valueFloat = nil 132 | } 133 | 134 | cells = append(cells, &sheets.CellData{ 135 | UserEnteredValue: &sheets.ExtendedValue{ 136 | NumberValue: valueFloat, 137 | }, 138 | }) 139 | 140 | dims := []string{} 141 | for k, v := range s.Metric { 142 | if k == model.MetricNameLabel { 143 | continue 144 | } 145 | dims = append(dims, fmt.Sprintf("%s: %s", k, v)) 146 | } 147 | sortedDims := sort.StringSlice(dims) 148 | sortedDims.Sort() 149 | 150 | cells = append(cells, &sheets.CellData{ 151 | UserEnteredValue: &sheets.ExtendedValue{ 152 | StringValue: ref(strings.Join(sortedDims, "\n")), 153 | }, 154 | }) 155 | 156 | return cells 157 | 158 | } 159 | 160 | type Config struct { 161 | Metrics []string 162 | RetireUntilRow int 163 | } 164 | 165 | func (c *Client) GetConfig(ctx context.Context) (*Config, error) { 166 | if err := c.rateLimiter.Wait(ctx); err != nil { 167 | return nil, err 168 | } 169 | req := c.svc.Spreadsheets.Values.Get(c.SpreadsheetID, "Config!A:B") 170 | res, err := req.Context(ctx).Do() 171 | if err != nil { 172 | return nil, err 173 | } 174 | 175 | conf := Config{} 176 | for _, row := range res.Values { 177 | if len(row) < 2 { 178 | continue 179 | } 180 | key := fmt.Sprintf("%s", row[0]) 181 | strVal := fmt.Sprintf("%s", row[1]) 182 | switch strings.ToUpper(key) { 183 | case "METRICS": 184 | val := strings.Split(strings.ToLower(strVal), "\n") 185 | conf.Metrics = val 186 | case "RETIRE_UNTIL_ROW": 187 | if val, err := strconv.Atoi(strVal); err != nil { 188 | log.Ctx(ctx).Err(err).Msgf("failed to parse %q as int for RETIRE_UNTIL_ROW", strVal) 189 | conf.RetireUntilRow = 1 190 | } else { 191 | conf.RetireUntilRow = val 192 | } 193 | } 194 | } 195 | 196 | return &conf, nil 197 | } 198 | 199 | func (c *Client) RetireMetrics(ctx context.Context) (int, error) { 200 | 201 | sortReq := c.svc.Spreadsheets.BatchUpdate(c.SpreadsheetID, &sheets.BatchUpdateSpreadsheetRequest{ 202 | Requests: []*sheets.Request{ 203 | { 204 | SortRange: &sheets.SortRangeRequest{ 205 | Range: &sheets.GridRange{}, 206 | SortSpecs: []*sheets.SortSpec{ 207 | { 208 | DimensionIndex: 0, 209 | SortOrder: "ASCENDING", 210 | }, 211 | }, 212 | }, 213 | }, 214 | }, 215 | ResponseIncludeGridData: true, 216 | }) 217 | if err := c.rateLimiter.Wait(ctx); err != nil { 218 | return 0, err 219 | } 220 | _, err := sortReq.Context(ctx).Do() 221 | if err != nil { 222 | return 0, err 223 | } 224 | 225 | conf, err := c.GetConfig(ctx) 226 | if err != nil { 227 | return 0, err 228 | } 229 | 230 | rowNumber := conf.RetireUntilRow 231 | rowNumber-- 232 | if rowNumber <= 0 { 233 | return 0, nil 234 | } 235 | 236 | if err := c.rateLimiter.Wait(ctx); err != nil { 237 | return 0, err 238 | } 239 | _, err = c.svc.Spreadsheets.BatchUpdate(c.SpreadsheetID, &sheets.BatchUpdateSpreadsheetRequest{ 240 | Requests: []*sheets.Request{ 241 | { 242 | DeleteDimension: &sheets.DeleteDimensionRequest{ 243 | Range: &sheets.DimensionRange{ 244 | Dimension: "ROWS", 245 | EndIndex: int64(rowNumber), 246 | }, 247 | ForceSendFields: nil, 248 | NullFields: nil, 249 | }, 250 | }, 251 | }, 252 | }).Context(ctx).Do() 253 | if err != nil { 254 | return 0, nil 255 | } 256 | 257 | return rowNumber, nil 258 | } 259 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= 4 | cloud.google.com/go/compute v1.19.0 h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ= 5 | cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= 6 | cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= 7 | cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= 8 | cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= 9 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 10 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 11 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 12 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 13 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 14 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 15 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 16 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 17 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 18 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 19 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 20 | github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= 21 | github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 22 | github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 23 | github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 24 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 25 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 28 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 29 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 30 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 31 | github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= 32 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 33 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 34 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 35 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 36 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 37 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 38 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 39 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 40 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 41 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 42 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 43 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 44 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 45 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 46 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 47 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 48 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 49 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 50 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 51 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 52 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 53 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 54 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 55 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 56 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 57 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 58 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 59 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 60 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 61 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 62 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 63 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 64 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 65 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 66 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 67 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 68 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 69 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 70 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 71 | github.com/google/s2a-go v0.1.3 h1:FAgZmpLl/SXurPEZyCMPBIiiYeTbqfjlbdnCNTAkbGE= 72 | github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= 73 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 74 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 75 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 76 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= 77 | github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= 78 | github.com/googleapis/gax-go/v2 v2.8.0 h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK67vJZc= 79 | github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= 80 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 81 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 82 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 83 | github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo= 84 | github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA= 85 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 86 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 87 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 88 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 89 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 90 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 91 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 92 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 93 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 94 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 95 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 96 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 97 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 98 | github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= 99 | github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= 100 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 101 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 102 | github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 103 | github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= 104 | github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= 105 | github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= 106 | github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= 107 | github.com/prometheus/prometheus v0.44.0 h1:sgn8Fdx+uE5tHQn0/622swlk2XnIj6udoZCnbVjHIgc= 108 | github.com/prometheus/prometheus v0.44.0/go.mod h1:aPsmIK3py5XammeTguyqTmuqzX/jeCdyOWWobLHNKQg= 109 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 110 | github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 111 | github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= 112 | github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= 113 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 114 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 115 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 116 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 117 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 118 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 119 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 120 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 121 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 122 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 123 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 124 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 125 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 126 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 127 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 128 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 129 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 130 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 131 | golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 132 | golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= 133 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 134 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 135 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= 136 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 137 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 138 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 139 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 140 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 141 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 142 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 143 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 144 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 145 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 146 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 147 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 148 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 149 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 150 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 151 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 152 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 153 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 154 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 155 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 156 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 157 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 158 | golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= 159 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 160 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 161 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 162 | golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= 163 | golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= 164 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 165 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 166 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 167 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 168 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 169 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 170 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 171 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 172 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 173 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 174 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 175 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 176 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 177 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 178 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 179 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 180 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 181 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 182 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 183 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 184 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 185 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 186 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 187 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 188 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 189 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 190 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 191 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 192 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 193 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 194 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 195 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 196 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 197 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 198 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 199 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 200 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 201 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 202 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 203 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 204 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 205 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 206 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 207 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 208 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 209 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 210 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 211 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 212 | google.golang.org/api v0.123.0 h1:yHVU//vA+qkOhm4reEC9LtzHVUCN/IqqNRl1iQ9xE20= 213 | google.golang.org/api v0.123.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= 214 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 215 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 216 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 217 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 218 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 219 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 220 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 221 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 222 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= 223 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= 224 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 225 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 226 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 227 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 228 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 229 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 230 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 231 | google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= 232 | google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= 233 | google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= 234 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 235 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 236 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 237 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 238 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 239 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 240 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 241 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 242 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 243 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 244 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 245 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 246 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 247 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 248 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 249 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 250 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 251 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 252 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 253 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 254 | --------------------------------------------------------------------------------