├── .gitignore ├── internal ├── app │ ├── provider │ │ ├── errors.go │ │ ├── provider.go │ │ ├── memory.go │ │ ├── memory_test.go │ │ ├── instrumentation_middleware.go │ │ ├── sql_test.go │ │ ├── sql.go │ │ └── instrumentation_middleware_test.go │ └── core │ │ ├── core_test.go │ │ └── core.go └── pkg │ ├── metrics │ ├── observer.go │ └── prometheus │ │ ├── server.go │ │ ├── histogram.go │ │ ├── prometheus_test.go │ │ └── histogram_test.go │ └── grpcserver │ └── server.go ├── cmd └── postviewd │ ├── main.go │ ├── root.go │ ├── version.go │ ├── prometheus.go │ ├── container.go │ ├── migrate.go │ ├── logging.go │ ├── config.go │ └── serve.go ├── pkg ├── sql │ ├── sql.go │ ├── sqlite.go │ ├── gorm.go │ └── postgres.go ├── cache │ ├── layer.go │ ├── multilayercache │ │ ├── multilayercache_test.go │ │ └── multilayercache.go │ ├── adaptors │ │ ├── redis_test.go │ │ ├── bigcache_test.go │ │ ├── bigcache.go │ │ ├── syncmap.go │ │ └── redis.go │ └── middlewares │ │ ├── instrumentation.go │ │ └── instrumentation_test.go ├── errors │ ├── interceptor.go │ ├── errors.go │ └── errors_test.go └── postview │ ├── version.go │ └── postview.pb.go ├── EMERGENCY.md ├── Dockerfile.build ├── Dockerfile ├── .golangci-lint.yml ├── Dockerfile.base ├── README.md ├── api └── postview │ └── postview.proto ├── go.mod ├── CONTRIBUTING.md ├── .gitlab-ci.yml ├── Makefile └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | /postviewd 2 | /.bin 3 | mocks 4 | .vscode/ 5 | .coverage 6 | *.cover 7 | /coverage.html 8 | .idea 9 | **/wire_gen.go 10 | -------------------------------------------------------------------------------- /internal/app/provider/errors.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotFound = errors.New("post not found") 7 | ) 8 | -------------------------------------------------------------------------------- /internal/pkg/metrics/observer.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | type Observer interface { 4 | Observe(float64) 5 | With(labels map[string]string) Observer 6 | } 7 | -------------------------------------------------------------------------------- /cmd/postviewd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func main() { 9 | if err := rootCmd.Execute(); err != nil { 10 | fmt.Println(err) 11 | os.Exit(-1) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pkg/sql/sql.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | type Config interface { 4 | ConnectionString() string 5 | Dialect() string 6 | GetMaxIDLEConnection() int 7 | GetMaxOpenConnection() int 8 | } 9 | 10 | type Migrater interface { 11 | Migrate() error 12 | } 13 | -------------------------------------------------------------------------------- /pkg/cache/layer.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "context" 4 | 5 | type Layer interface { 6 | Get(ctx context.Context, key string) ([]byte, error) 7 | Set(ctx context.Context, key string, value []byte) error 8 | Delete(ctx context.Context, key string) error 9 | Clear(ctx context.Context) error 10 | } 11 | -------------------------------------------------------------------------------- /EMERGENCY.md: -------------------------------------------------------------------------------- 1 | # Emergency todo list 2 | 3 | `Here we should explain how to detect problem when a failure happen` 4 | 5 | Do you think queue-reader may have a problem? here's what you should do. 6 | 7 | - Keep calm 8 | 9 | - Make sure you don't do any write actions without letting others know, unless "others" are nowhere to be seen. 10 | -------------------------------------------------------------------------------- /Dockerfile.build: -------------------------------------------------------------------------------- 1 | FROM BASE_IMAGE_TAG AS base_image 2 | 3 | ARG CI_JOB_TOKEN 4 | COPY . /srv/build 5 | 6 | RUN \ 7 | git config --global credential.helper store && \ 8 | echo "https://gitlab-ci-token:${CI_JOB_TOKEN}@git.cafebazaar.ir" >> ~/.git-credentials && \ 9 | PROTOC=/srv/protoc3/bin/protoc PROTOC_OPTIONS="-I/srv/protoc3/include -I." make generate && \ 10 | make postviewd 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM BUILD_IMAGE_TAG as build_image 2 | 3 | FROM ubuntu:18.04 4 | 5 | RUN apt update --fix-missing && \ 6 | apt-get upgrade -y && \ 7 | apt install -y ca-certificates && \ 8 | apt install -y tzdata && \ 9 | ln -sf /usr/share/zoneinfo/UTC /etc/localtime && \ 10 | dpkg-reconfigure -f noninteractive tzdata && \ 11 | apt-get clean 12 | 13 | COPY --from=build_image /srv/build/postviewd /bin/ 14 | 15 | ENTRYPOINT ["/bin/postviewd", "serve"] 16 | -------------------------------------------------------------------------------- /cmd/postviewd/root.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var rootCmd = &cobra.Command{ 8 | Use: "postviewd serve", 9 | Short: "serves posts to be viewed by users", 10 | Long: "Serves posts from a local database which is accompanied by a cache", 11 | Run: nil, 12 | } 13 | 14 | func init() { 15 | cobra.OnInitialize() 16 | rootCmd.PersistentFlags().StringP("config-file", "c", "", 17 | "Path to the config file (eg ./config.yaml) [Optional]") 18 | } 19 | -------------------------------------------------------------------------------- /pkg/sql/sqlite.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | type SqliteConfig struct { 4 | FileName string 5 | InMemory bool 6 | } 7 | 8 | func (c SqliteConfig) ConnectionString() string { 9 | if c.InMemory { 10 | return ":memory:" 11 | } 12 | 13 | return c.FileName 14 | } 15 | 16 | func (c SqliteConfig) Dialect() string { 17 | return "sqlite3" 18 | } 19 | 20 | func (c SqliteConfig) GetMaxIDLEConnection() int { 21 | return 1 22 | } 23 | 24 | func (c SqliteConfig) GetMaxOpenConnection() int { 25 | return 1 26 | } 27 | -------------------------------------------------------------------------------- /internal/app/provider/provider.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | context "context" 5 | 6 | "github.com/cafebazaar/go-boilerplate/pkg/postview" 7 | ) 8 | 9 | // PostProvider specifies mechanism of retrieving posts. 10 | // which can be either a DB, some microservice, etc. 11 | type PostProvider interface { 12 | // Get post with requested token 13 | GetPost(ctx context.Context, token string) (*postview.Post, error) 14 | 15 | // Add post to datastore 16 | AddPost(ctx context.Context, post *postview.Post) error 17 | } 18 | -------------------------------------------------------------------------------- /pkg/errors/interceptor.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" 7 | 8 | "google.golang.org/grpc" 9 | ) 10 | 11 | func UnaryServerInterceptor(ctx context.Context, 12 | req interface{}, 13 | info *grpc.UnaryServerInfo, 14 | handler grpc.UnaryHandler) (resp interface{}, err error) { 15 | 16 | defer func() { 17 | if err != nil { 18 | ctxlogrus.AddFields(ctx, Extras(err)) 19 | } 20 | }() 21 | 22 | return handler(ctx, req) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/postviewd/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cafebazaar/go-boilerplate/pkg/postview" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var versionCmd = &cobra.Command{ 11 | Use: "version", 12 | Short: "Print the version info", 13 | Long: `All softwares have versions. This is aggregator`, 14 | Run: func(cmd *cobra.Command, args []string) { 15 | printVersion() 16 | }, 17 | } 18 | 19 | func init() { 20 | rootCmd.AddCommand(versionCmd) 21 | } 22 | 23 | func printVersion() { 24 | fmt.Printf("%-18s %-18s Commit:%s (%s)\n", postview.Title, postview.Version, 25 | postview.Commit, postview.BuildTime) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/sql/gorm.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import "github.com/jinzhu/gorm" 4 | 5 | func GetDatabase(config Config) (*gorm.DB, error) { 6 | db, err := gorm.Open(config.Dialect(), config.ConnectionString()) 7 | if err != nil { 8 | return nil, err 9 | } 10 | 11 | maxIDLEConnection := 10 12 | if config.GetMaxIDLEConnection() != 0 { 13 | maxIDLEConnection = config.GetMaxIDLEConnection() 14 | } 15 | db.DB().SetMaxIdleConns(maxIDLEConnection) 16 | 17 | maxOpenConnection := 100 18 | if config.GetMaxOpenConnection() != 0 { 19 | maxOpenConnection = config.GetMaxOpenConnection() 20 | } 21 | 22 | db.DB().SetMaxIdleConns(maxOpenConnection) 23 | 24 | return db, nil 25 | } 26 | -------------------------------------------------------------------------------- /.golangci-lint.yml: -------------------------------------------------------------------------------- 1 | # options for analysis running 2 | run: 3 | concurrency: 1 4 | deadline: 5m 5 | 6 | # include test files or not 7 | tests: true 8 | 9 | skip-dirs: 10 | - mocks 11 | 12 | # output configuration options 13 | output: 14 | # colored-line-number|line-number|json|tab|checkstyle 15 | format: colored-line-number 16 | 17 | # all available settings of specific linters 18 | linters-settings: 19 | lll: 20 | line-length: 120 21 | 22 | linters: 23 | enable: 24 | - golint 25 | - errcheck 26 | - megacheck 27 | - deadcode 28 | - varcheck 29 | - structcheck 30 | - unconvert 31 | - ineffassign 32 | - gofmt 33 | - lll 34 | - gosec 35 | - interfacer 36 | - govet 37 | - gosimple 38 | -------------------------------------------------------------------------------- /cmd/postviewd/prometheus.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/cafebazaar/go-boilerplate/internal/pkg/metrics" 5 | "github.com/cafebazaar/go-boilerplate/internal/pkg/metrics/prometheus" 6 | ) 7 | 8 | var ( 9 | cacheMetrics metrics.Observer 10 | postProviderMetrics metrics.Observer 11 | ) 12 | 13 | func init() { 14 | cacheMetrics = prometheus.NewHistogram("divar_post_view_cache", 15 | "view metrics about cache", "cache_type", "method", "ok", "success") 16 | 17 | postProviderMetrics = prometheus.NewHistogram("divar_post_view_provider", 18 | "view metrics about post provider", "provider_type", "method", "ok", "success") 19 | } 20 | 21 | func providePrometheus(config *Config) *prometheus.Server { 22 | return prometheus.NewServer(config.MetricListenPort) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/cache/multilayercache/multilayercache_test.go: -------------------------------------------------------------------------------- 1 | package multilayercache 2 | 3 | import ( 4 | "context" 5 | "github.com/cafebazaar/go-boilerplate/pkg/cache/adaptors" 6 | "github.com/sirupsen/logrus" 7 | "github.com/stretchr/testify/assert" 8 | "testing" 9 | ) 10 | 11 | func TestSetGet(t *testing.T) { 12 | 13 | var err error 14 | 15 | multiLayerCache := New(adaptors.NewSynMapAdaptor(logrus.New())) 16 | 17 | key := "test-key" 18 | value := "test-value" 19 | err = multiLayerCache.Set(context.Background(), key, []byte(value)) 20 | assert.NoError(t, err, "fail to set data") 21 | 22 | gottenValue, err := multiLayerCache.Get(context.Background(), key) 23 | assert.NoError(t, err, "fail to get data") 24 | 25 | assert.Equal(t, value, string(gottenValue), "gotten value is not equal to set value") 26 | } 27 | -------------------------------------------------------------------------------- /pkg/postview/version.go: -------------------------------------------------------------------------------- 1 | package postview 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | var ( 11 | Version string 12 | Commit string 13 | BuildTime string 14 | Title string 15 | StartTime time.Time 16 | Hostname string 17 | ) 18 | 19 | func init() { 20 | // If version, commit, or build time are not set, make that clear. 21 | if Version == "" { 22 | Version = "unknown" 23 | } 24 | if Commit == "" { 25 | Commit = "unknown" 26 | } 27 | if BuildTime == "" { 28 | BuildTime = "unknown" 29 | } 30 | if Title == "" { 31 | Title = "postview" 32 | } 33 | 34 | StartTime = time.Now() 35 | 36 | var err error 37 | Hostname, err = os.Hostname() 38 | if err != nil { 39 | logrus.WithError(err).Warn("Failed to set Runtime Hostname") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Dockerfile.base: -------------------------------------------------------------------------------- 1 | FROM golang:1.12 AS builder 2 | 3 | WORKDIR /srv/build 4 | 5 | ARG http_proxy 6 | ARG HTTP_PROXY 7 | ARG https_proxy 8 | ARG HTTPS_PROXY 9 | ARG no_proxy 10 | ARG NO_PROXY 11 | 12 | RUN \ 13 | apt update --fix-missing && \ 14 | apt-get install -y unzip && \ 15 | curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v3.7.1/protoc-3.7.1-linux-x86_64.zip && \ 16 | unzip protoc-3.7.1-linux-x86_64.zip -d /srv/protoc3 17 | 18 | RUN \ 19 | go get -v github.com/golang/protobuf/protoc-gen-go && \ 20 | go get -v github.com/vektra/mockery/cmd/mockery && \ 21 | go get -v github.com/axw/gocov/gocov && \ 22 | curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b /bin/ v1.12.5 23 | 24 | ADD go.mod go.sum /srv/build/ 25 | RUN go mod download | true 26 | -------------------------------------------------------------------------------- /cmd/postviewd/container.go: -------------------------------------------------------------------------------- 1 | //+build wireinject 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "github.com/cafebazaar/go-boilerplate/internal/app/core" 8 | "github.com/cafebazaar/go-boilerplate/internal/pkg/grpcserver" 9 | "github.com/cafebazaar/go-boilerplate/internal/app/provider" 10 | "github.com/google/wire" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func CreateServer(ctx context.Context, cmd *cobra.Command) (*grpcserver.Server, error) { 15 | panic(wire.Build( 16 | provideConfig, 17 | provideLogger, 18 | provideProvider, 19 | provideCache, 20 | providePrometheus, 21 | provideServer, 22 | core.New, 23 | )) 24 | } 25 | 26 | func CreateProvider(ctx context.Context, cmd *cobra.Command) (provider.PostProvider, error) { 27 | panic(wire.Build( 28 | provideConfig, 29 | provideLogger, 30 | provideProvider, 31 | providePrometheus, 32 | )) 33 | } 34 | -------------------------------------------------------------------------------- /internal/app/provider/memory.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/cafebazaar/go-boilerplate/pkg/errors" 8 | "github.com/cafebazaar/go-boilerplate/pkg/postview" 9 | ) 10 | 11 | type memoryProvider struct { 12 | items sync.Map 13 | } 14 | 15 | func NewMemory() PostProvider { 16 | return &memoryProvider{} 17 | } 18 | 19 | func (provider *memoryProvider) GetPost(ctx context.Context, token string) (*postview.Post, error) { 20 | value, ok := provider.items.Load(token) 21 | if !ok { 22 | return nil, errors.WrapWithExtra(ErrNotFound, "post not found", map[string]interface{}{ 23 | "token": token, 24 | }) 25 | } 26 | 27 | return value.(*postview.Post), nil 28 | } 29 | 30 | func (provider *memoryProvider) AddPost(ctx context.Context, post *postview.Post) error { 31 | provider.items.Store(post.Token, post) 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Divar Post API 2 | A sample gRPC server with Go inspired by Divar's postview microservice. 3 | This project aims to share best practices and design patterns for writing a new go microservice with a good quality that meets our requirements inside CafeBazaar. 4 | 5 | You have find a better pattern or library? Let us know by creating a new issue. 6 | 7 | ## How to run: 8 | Compile the code 9 | ``` 10 | make postviewd 11 | ``` 12 | Then Run the binary 13 | ``` 14 | ./postviewd serve 15 | ``` 16 | 17 | # Emergency 18 | In case of emergencies checkout our [Emergency ABC](EMERGENCY.md). 19 | 20 | # How does it work? 21 | `Use this section in order to explain how your system works` 22 | 23 | # Development 24 | If you want to contribute to this code, please read `CONTRIBUTING.md` first. 25 | 26 | # Dependency Graph 27 | ```mermaid 28 | graph LR; 29 | A --> B; 30 | A --> C; 31 | ``` 32 | -------------------------------------------------------------------------------- /api/postview/postview.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | // protoc -I. --go_out=plugins=grpc:$GOPATH/src ./api/postview/postview.proto 4 | 5 | package postview; 6 | option go_package = "github.com/cafebazaar/go-boilerplate/pkg/postview"; 7 | 8 | import "google/protobuf/timestamp.proto"; 9 | 10 | message GetPostRequest { 11 | string token = 1; 12 | } 13 | 14 | message GetPostResponse { 15 | Post post = 1; 16 | } 17 | 18 | message Post { 19 | string title = 1; 20 | string description = 2; 21 | string token = 3; 22 | string thumbnail = 4; 23 | Contact contact = 5; 24 | google.protobuf.Timestamp published_at = 6; 25 | repeated string category_slugs = 7; 26 | } 27 | 28 | message Contact { 29 | string phone = 1; 30 | string email = 2; 31 | bool chat = 4; 32 | } 33 | 34 | service PostView { 35 | rpc GetPost(GetPostRequest) returns (GetPostResponse); 36 | } 37 | 38 | -------------------------------------------------------------------------------- /cmd/postviewd/migrate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/cafebazaar/go-boilerplate/pkg/sql" 5 | "github.com/sirupsen/logrus" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var migrateCmd = &cobra.Command{ 10 | Use: "migrate", 11 | Short: "Run necessary database migrations", 12 | Long: `Migrate database to latest schema version`, 13 | Run: migrateDatabase, 14 | } 15 | 16 | func init() { 17 | rootCmd.AddCommand(migrateCmd) 18 | } 19 | 20 | func migrateDatabase(cmd *cobra.Command, args []string) { 21 | printVersion() 22 | 23 | config, err := provideConfig(cmd) 24 | panicWithError(err, "fail to create provider") 25 | prometheus := providePrometheus(config) 26 | providerInstance := provideProvider(config, logrus.New(), prometheus) 27 | migrater, ok := providerInstance.(sql.Migrater) 28 | if ok { 29 | err := migrater.Migrate() 30 | if err != nil { 31 | logrus.WithError(err).Panic("failed to migrate datbase") 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/sql/postgres.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import "fmt" 4 | 5 | type PostgresConfig struct { 6 | Host string 7 | Port int 8 | Username string 9 | Password string 10 | Database string 11 | SSL bool 12 | MaxIDLEConnection int 13 | MaxOpenConnection int 14 | } 15 | 16 | func (c PostgresConfig) ConnectionString() string { 17 | sslMode := "enable" 18 | if !c.SSL { 19 | sslMode = "disable" 20 | } 21 | 22 | return fmt.Sprintf( 23 | "host=%s port=%d user=%s dbname=%s password=%s sslmode=%s binary_parameters=yes", 24 | c.Host, 25 | c.Port, 26 | c.Username, 27 | c.Database, 28 | c.Password, 29 | sslMode, 30 | ) 31 | } 32 | 33 | func (c PostgresConfig) Dialect() string { 34 | return "postgres" 35 | } 36 | 37 | func (c PostgresConfig) GetMaxIDLEConnection() int { 38 | return c.MaxIDLEConnection 39 | } 40 | 41 | func (c PostgresConfig) GetMaxOpenConnection() int { 42 | return c.MaxOpenConnection 43 | } 44 | -------------------------------------------------------------------------------- /internal/pkg/metrics/prometheus/server.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/cafebazaar/go-boilerplate/pkg/errors" 9 | 10 | "github.com/prometheus/client_golang/prometheus/promhttp" 11 | ) 12 | 13 | type Server struct { 14 | httpServer *http.Server 15 | } 16 | 17 | func NewServer(listenPort int) *Server { 18 | server := &http.Server{ 19 | Addr: fmt.Sprintf(":%d", listenPort), 20 | Handler: promhttp.Handler(), 21 | } 22 | 23 | return &Server{ 24 | httpServer: server, 25 | } 26 | } 27 | 28 | func (s *Server) Serve() error { 29 | if err := s.httpServer.ListenAndServe(); err != nil { 30 | return errors.Wrap(err, "failed to start Prometheus http listener") 31 | } 32 | return nil 33 | } 34 | 35 | func (s *Server) Stop(server *http.Server) error { 36 | if err := server.Shutdown(context.Background()); err != nil { 37 | return errors.Wrap(err, "Failed to shutdown prometheus metric server") 38 | } 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cafebazaar/go-boilerplate 2 | 3 | require ( 4 | github.com/allegro/bigcache v1.2.1 5 | github.com/axw/gocov v1.0.0 // indirect 6 | github.com/certifi/gocertifi v0.0.0-20190506164543-d2eda7129713 // indirect 7 | github.com/evalphobia/logrus_sentry v0.8.2 8 | github.com/getsentry/raven-go v0.2.0 9 | github.com/go-logfmt/logfmt v0.4.0 // indirect 10 | github.com/go-redis/redis v6.15.2+incompatible 11 | github.com/golang/protobuf v1.3.1 12 | github.com/google/wire v0.3.0 13 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 14 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 15 | github.com/jinzhu/gorm v1.9.8 16 | github.com/pkg/errors v0.8.0 17 | github.com/prometheus/client_golang v0.9.4 18 | github.com/sirupsen/logrus v1.4.2 19 | github.com/spf13/cobra v0.0.5 20 | github.com/spf13/viper v1.4.0 21 | github.com/stretchr/testify v1.3.0 22 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 23 | google.golang.org/grpc v1.21.1 24 | gopkg.in/gormigrate.v1 v1.5.0 25 | ) 26 | -------------------------------------------------------------------------------- /pkg/cache/adaptors/redis_test.go: -------------------------------------------------------------------------------- 1 | package adaptors 2 | 3 | //import ( 4 | // "context" 5 | // "github.com/go-redis/redis" 6 | // "github.com/stretchr/testify/assert" 7 | // "os" 8 | // "testing" 9 | // "time" 10 | //) 11 | // 12 | //func TestRedisSetGet(t *testing.T) { 13 | // redisAddr, ok := os.LookupEnv("MULTILAYERCACHE_REDIS_ADDR") 14 | // if !ok { 15 | // redisAddr = "127.0.0.1:6379" 16 | // } 17 | // redisClient := redis.NewClient(&redis.Options{ 18 | // Addr: redisAddr, 19 | // DB: 3, 20 | // }) 21 | // // Ping Redis 22 | // err := redisClient.Ping().Err() 23 | // if !assert.NoError(t, err) { 24 | // t.Fatal("fail to connect to redis") 25 | // } 26 | // redisAdaptor := NewRedisAdaptor(1*time.Minute, redisClient) 27 | // 28 | // key := "test-key" 29 | // value := "test-value" 30 | // err = redisAdaptor.Set(context.Background(), key, []byte(value)) 31 | // assert.NoError(t, err, "fail to set data") 32 | // 33 | // gottenValue, err := redisAdaptor.Get(context.Background(), key) 34 | // assert.NoError(t, err, "fail to get data") 35 | // 36 | // assert.Equal(t, value, string(gottenValue), "gotten value is not equal to set value") 37 | //} 38 | -------------------------------------------------------------------------------- /pkg/cache/adaptors/bigcache_test.go: -------------------------------------------------------------------------------- 1 | package adaptors 2 | 3 | import ( 4 | "context" 5 | "github.com/allegro/bigcache" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestBigCacheSetGet(t *testing.T) { 12 | bigCacheInstance, err := bigcache.NewBigCache(bigcache.Config{ 13 | Shards: 1, 14 | LifeWindow: 1 * time.Minute, 15 | MaxEntriesInWindow: 1100, 16 | MaxEntrySize: 1000, 17 | Verbose: true, 18 | HardMaxCacheSize: 100, 19 | }) 20 | if !assert.NoError(t, err) { 21 | t.Fatal("fail to initialize big cache") 22 | } 23 | 24 | bigCacheAdaptor := NewBigCacheAdaptor(bigCacheInstance) 25 | 26 | if !assert.NoError(t, err) { 27 | t.Fatal("fail to initialize adaptor") 28 | } 29 | 30 | key := "test-key" 31 | value := "test-value" 32 | 33 | err = bigCacheAdaptor.Set(context.Background(), key, []byte(value)) 34 | assert.NoError(t, err, "fail to set data") 35 | 36 | gottenValue, err := bigCacheAdaptor.Get(context.Background(), key) 37 | assert.NoError(t, err, "fail to get data") 38 | assert.Equal(t, value, string(gottenValue), "gotten value is not equal to set value") 39 | } 40 | -------------------------------------------------------------------------------- /internal/app/provider/memory_test.go: -------------------------------------------------------------------------------- 1 | package provider_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/cafebazaar/go-boilerplate/pkg/postview" 8 | "github.com/stretchr/testify/suite" 9 | "golang.org/x/xerrors" 10 | ) 11 | 12 | type MemoryProviderTestSuite struct { 13 | suite.Suite 14 | 15 | provider PostProvider 16 | } 17 | 18 | func TestMemoryProviderTestSuite(t *testing.T) { 19 | suite.Run(t, new(MemoryProviderTestSuite)) 20 | } 21 | 22 | func (s *MemoryProviderTestSuite) TestShouldReturnNotFoundInitially() { 23 | _, err := s.provider.GetPost(context.Background(), "token") 24 | s.True(xerrors.Is(err, ErrNotFound)) 25 | } 26 | 27 | func (s *MemoryProviderTestSuite) TestShouldReturnPostAfterAdd() { 28 | err := s.provider.AddPost(context.Background(), &postview.Post{ 29 | Token: "abcd", 30 | Title: "a title", 31 | }) 32 | s.Nil(err) 33 | if err != nil { 34 | return 35 | } 36 | 37 | post, err := s.provider.GetPost(context.Background(), "abcd") 38 | s.Nil(err) 39 | if err != nil { 40 | return 41 | } 42 | 43 | s.Equal("abcd", post.Token) 44 | s.Equal("a title", post.Title) 45 | } 46 | 47 | func (s *MemoryProviderTestSuite) SetupTest() { 48 | s.provider = NewMemory() 49 | } 50 | -------------------------------------------------------------------------------- /pkg/cache/adaptors/bigcache.go: -------------------------------------------------------------------------------- 1 | package adaptors 2 | 3 | import ( 4 | "context" 5 | "github.com/allegro/bigcache" 6 | "github.com/cafebazaar/go-boilerplate/pkg/cache" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type bigCache struct { 11 | ins *bigcache.BigCache 12 | } 13 | 14 | func NewBigCacheAdaptor(instance *bigcache.BigCache) cache.Layer { 15 | ins := &bigCache{ 16 | ins: instance, 17 | } 18 | return ins 19 | } 20 | 21 | func (c *bigCache) Get(_ context.Context, key string) ([]byte, error) { 22 | if c == nil { 23 | return nil, errors.New("free cache is disabled") 24 | } 25 | value, err := c.ins.Get(key) 26 | return value, err 27 | } 28 | 29 | func (c *bigCache) Set(_ context.Context, key string, value []byte) error { 30 | if c == nil { 31 | return errors.New("free cache is disabled") 32 | } 33 | err := c.ins.Set(key, value) 34 | return err 35 | } 36 | 37 | func (c *bigCache) Delete(_ context.Context, key string) error { 38 | if c == nil { 39 | return errors.New("inmem cache is disabled") 40 | } 41 | err := c.ins.Delete(key) 42 | return err 43 | } 44 | 45 | func (c *bigCache) Clear(_ context.Context) error { 46 | if c == nil { 47 | return errors.New("inmem cache is disabled") 48 | } 49 | err := c.ins.Reset() 50 | return err 51 | } 52 | -------------------------------------------------------------------------------- /cmd/postviewd/logging.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/evalphobia/logrus_sentry" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // LoggingConfig the loggers's configuration structure 11 | type LoggingConfig struct { 12 | Level string 13 | SentryEnabled bool 14 | } 15 | 16 | const ( 17 | sentryDSN = "http://xxxx:xxxx@sentry.cafebazaar.ir/112" 18 | ) 19 | 20 | // ConfigureLogging handlerconfig logger based on the given configuration 21 | func provideLogger(config *Config) (*logrus.Logger, error) { 22 | logger := logrus.New() 23 | if config.Logging.Level != "" { 24 | level, err := logrus.ParseLevel(config.Logging.Level) 25 | if err != nil { 26 | return nil, err 27 | } 28 | logger.SetLevel(level) 29 | } 30 | 31 | logger.SetFormatter(&logrus.JSONFormatter{ 32 | DisableTimestamp: false, 33 | }) 34 | 35 | if config.Logging.SentryEnabled { 36 | 37 | hook, err := logrus_sentry.NewAsyncSentryHook(sentryDSN, []logrus.Level{ 38 | logrus.PanicLevel, 39 | logrus.FatalLevel, 40 | logrus.ErrorLevel, 41 | }) 42 | 43 | if err != nil { 44 | fmt.Println(err) 45 | panic("failed to setup raven!") 46 | } 47 | 48 | hook.StacktraceConfiguration.Enable = true 49 | 50 | logger.AddHook(hook) 51 | } 52 | 53 | return logger, nil 54 | } 55 | -------------------------------------------------------------------------------- /pkg/cache/adaptors/syncmap.go: -------------------------------------------------------------------------------- 1 | package adaptors 2 | 3 | import ( 4 | "context" 5 | "github.com/cafebazaar/go-boilerplate/pkg/cache" 6 | "github.com/pkg/errors" 7 | "github.com/sirupsen/logrus" 8 | "sync" 9 | ) 10 | 11 | type syncMap struct { 12 | data sync.Map 13 | logger *logrus.Logger 14 | } 15 | 16 | func NewSynMapAdaptor(logger *logrus.Logger) cache.Layer { 17 | logger.Info("new cache initialized") 18 | return &syncMap{ 19 | data: sync.Map{}, 20 | logger: logger, 21 | } 22 | } 23 | 24 | func (cache *syncMap) Get(_ context.Context, key string) ([]byte, error) { 25 | value, ok := cache.data.Load(key) 26 | if !ok { 27 | cache.logger.Infof("key %s not found", key) 28 | return nil, errors.New("not found") 29 | } 30 | cache.logger.Infof("get key %s successfully", key) 31 | return value.([]byte), nil 32 | } 33 | func (cache *syncMap) Delete(_ context.Context, key string) error { 34 | cache.logger.Infof("key %s is deleted", key) 35 | cache.data.Delete(key) 36 | return nil 37 | } 38 | 39 | func (cache *syncMap) Set(_ context.Context, key string, value []byte) error { 40 | cache.logger.Infof("key %s is updated", key) 41 | cache.data.Store(key, value) 42 | return nil 43 | } 44 | 45 | func (cache *syncMap) Clear(_ context.Context) error { 46 | cache.logger.Infof("clearing") 47 | cache.data = sync.Map{} 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/app/provider/instrumentation_middleware.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/cafebazaar/go-boilerplate/internal/pkg/metrics" 9 | "github.com/cafebazaar/go-boilerplate/pkg/postview" 10 | ) 11 | 12 | type instrumentationMiddleware struct { 13 | next PostProvider 14 | timing metrics.Observer 15 | } 16 | 17 | func NewInstrumentationMiddleware( 18 | next PostProvider, timing metrics.Observer) PostProvider { 19 | 20 | return instrumentationMiddleware{ 21 | next: next, 22 | timing: timing, 23 | } 24 | } 25 | 26 | func (m instrumentationMiddleware) GetPost( 27 | ctx context.Context, token string) (result *postview.Post, err error) { 28 | 29 | defer func(startTime time.Time) { 30 | m.timing.With(map[string]string{ 31 | "success": fmt.Sprint(err == nil), 32 | "method": "GetPost", 33 | }).Observe(time.Since(startTime).Seconds()) 34 | }(time.Now()) 35 | 36 | return m.next.GetPost(ctx, token) 37 | } 38 | 39 | func (m instrumentationMiddleware) AddPost( 40 | ctx context.Context, post *postview.Post) (err error) { 41 | 42 | defer func(startTime time.Time) { 43 | m.timing.With(map[string]string{ 44 | "success": fmt.Sprint(err == nil), 45 | "method": "AddPost", 46 | }).Observe(time.Since(startTime).Seconds()) 47 | }(time.Now()) 48 | 49 | return m.next.AddPost(ctx, post) 50 | } 51 | -------------------------------------------------------------------------------- /internal/pkg/metrics/prometheus/histogram.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "github.com/cafebazaar/go-boilerplate/internal/pkg/metrics" 5 | "github.com/prometheus/client_golang/prometheus" 6 | ) 7 | 8 | type histogram struct { 9 | vec *prometheus.HistogramVec 10 | labels []string 11 | labelValues map[string]string 12 | } 13 | 14 | func NewHistogram(name, help string, labels ...string) metrics.Observer { 15 | result := &histogram{ 16 | vec: prometheus.NewHistogramVec(prometheus.HistogramOpts{ 17 | Name: name, 18 | Help: help, 19 | }, labels), 20 | 21 | labels: labels, 22 | labelValues: make(map[string]string), 23 | } 24 | 25 | prometheus.MustRegister(result.vec) 26 | 27 | return result 28 | } 29 | 30 | func (s *histogram) Observe(value float64) { 31 | values := make([]string, len(s.labels)) 32 | 33 | for i, label := range s.labels { 34 | values[i] = s.labelValues[label] 35 | } 36 | 37 | s.vec.WithLabelValues(values...).Observe(value) 38 | } 39 | 40 | func (s *histogram) With(labels map[string]string) metrics.Observer { 41 | newLabelValues := make(map[string]string) 42 | 43 | for label, value := range s.labelValues { 44 | newLabelValues[label] = value 45 | } 46 | 47 | for label, value := range labels { 48 | newLabelValues[label] = value 49 | } 50 | 51 | return &histogram{ 52 | labelValues: newLabelValues, 53 | labels: s.labels, 54 | vec: s.vec, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pkg/cache/adaptors/redis.go: -------------------------------------------------------------------------------- 1 | package adaptors 2 | 3 | import ( 4 | "context" 5 | "github.com/cafebazaar/go-boilerplate/pkg/cache" 6 | "github.com/go-redis/redis" 7 | "github.com/pkg/errors" 8 | "time" 9 | ) 10 | 11 | type redisCache struct { 12 | ins *redis.Client 13 | expireTime time.Duration 14 | } 15 | 16 | func NewRedisAdaptor(expireTime time.Duration, redisClient *redis.Client) cache.Layer { 17 | return &redisCache{ 18 | expireTime: expireTime, 19 | ins: redisClient, 20 | } 21 | } 22 | 23 | func (rc *redisCache) Get(ctx context.Context, key string) ([]byte, error) { 24 | if rc == nil { 25 | return nil, errors.New("redis cache is disabled") 26 | } 27 | client := rc.ins.WithContext(ctx) 28 | value, err := client.Get(key).Result() 29 | return []byte(value), err 30 | } 31 | 32 | func (rc *redisCache) Set(ctx context.Context, key string, value []byte) error { 33 | if rc == nil { 34 | return errors.New("redis cache is disabled") 35 | } 36 | client := rc.ins.WithContext(ctx) 37 | err := client.SetNX(key, value, rc.expireTime).Err() 38 | return err 39 | } 40 | 41 | func (rc *redisCache) Delete(ctx context.Context, key string) error { 42 | if rc == nil { 43 | return errors.New("redis cache is disable") 44 | } 45 | client := rc.ins.WithContext(ctx) 46 | err := client.Del(key).Err() 47 | return err 48 | } 49 | 50 | func (rc *redisCache) Clear(ctx context.Context) error { 51 | if rc == nil { 52 | return errors.New("redis client is disable") 53 | } 54 | client := rc.ins.WithContext(ctx) 55 | err := client.FlushDB().Err() 56 | return err 57 | } 58 | -------------------------------------------------------------------------------- /pkg/cache/middlewares/instrumentation.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/cafebazaar/go-boilerplate/pkg/cache" 7 | "time" 8 | 9 | "github.com/cafebazaar/go-boilerplate/internal/pkg/metrics" 10 | ) 11 | 12 | type instrumentationMiddleware struct { 13 | next cache.Layer 14 | timing metrics.Observer 15 | } 16 | 17 | func NewInstrumentationMiddleware(layer cache.Layer, timing metrics.Observer) cache.Layer { 18 | return instrumentationMiddleware{ 19 | next: layer, 20 | timing: timing, 21 | } 22 | } 23 | 24 | func (m instrumentationMiddleware) Get(ctx context.Context, key string) (data []byte, err error) { 25 | defer func(startTime time.Time) { 26 | m.timing.With(map[string]string{ 27 | "success": fmt.Sprint(err == nil), 28 | "method": "Get", 29 | }).Observe(time.Since(startTime).Seconds()) 30 | }(time.Now()) 31 | 32 | return m.next.Get(ctx, key) 33 | } 34 | 35 | func (m instrumentationMiddleware) Set(ctx context.Context, key string, value []byte) (err error) { 36 | defer func(startTime time.Time) { 37 | m.timing.With(map[string]string{ 38 | "success": fmt.Sprint(err == nil), 39 | "method": "Set", 40 | }).Observe(time.Since(startTime).Seconds()) 41 | }(time.Now()) 42 | 43 | return m.next.Set(ctx, key, value) 44 | } 45 | 46 | func (m instrumentationMiddleware) Delete(ctx context.Context, key string) (err error) { 47 | defer func(startTime time.Time) { 48 | m.timing.With(map[string]string{ 49 | "success": fmt.Sprint(err == nil), 50 | "method": "Delete", 51 | }).Observe(time.Since(startTime).Seconds()) 52 | }(time.Now()) 53 | 54 | return m.next.Delete(ctx, key) 55 | } 56 | 57 | func (m instrumentationMiddleware) Clear(ctx context.Context) (err error) { 58 | return m.next.Clear(ctx) 59 | } 60 | -------------------------------------------------------------------------------- /internal/pkg/grpcserver/server.go: -------------------------------------------------------------------------------- 1 | package grpcserver 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cafebazaar/go-boilerplate/pkg/errors" 6 | "github.com/cafebazaar/go-boilerplate/pkg/postview" 7 | grpcmiddleware "github.com/grpc-ecosystem/go-grpc-middleware" 8 | grpclogrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" 9 | grpcrecovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" 10 | grpcprometheus "github.com/grpc-ecosystem/go-grpc-prometheus" 11 | "github.com/sirupsen/logrus" 12 | "google.golang.org/grpc" 13 | "net" 14 | ) 15 | 16 | type Server struct { 17 | listener net.Listener 18 | server *grpc.Server 19 | } 20 | 21 | func New(postViewServer postview.PostViewServer, logger *logrus.Logger, listenPort int) (*Server, error) { 22 | listener, err := net.Listen("tcp", fmt.Sprintf(":%d", listenPort)) 23 | if err != nil { 24 | return nil, errors.Wrap(err, "failed to listen") 25 | } 26 | 27 | logEntry := logger.WithFields(map[string]interface{}{ 28 | "app": "postviewd", 29 | }) 30 | 31 | interceptors := []grpc.UnaryServerInterceptor{ 32 | grpclogrus.UnaryServerInterceptor(logEntry), 33 | errors.UnaryServerInterceptor, 34 | grpcprometheus.UnaryServerInterceptor, 35 | grpcrecovery.UnaryServerInterceptor(), 36 | } 37 | 38 | grpcServer := grpc.NewServer(grpc.UnaryInterceptor(grpcmiddleware.ChainUnaryServer(interceptors...))) 39 | postview.RegisterPostViewServer(grpcServer, postViewServer) 40 | 41 | return &Server{ 42 | listener: listener, 43 | server: grpcServer, 44 | }, nil 45 | } 46 | 47 | func (s *Server) Serve() error { 48 | if err := s.server.Serve(s.listener); err != nil { 49 | return errors.Wrap(err, "failed to serve") 50 | } 51 | return nil 52 | } 53 | 54 | func (s *Server) Stop() { 55 | s.server.GracefulStop() 56 | } 57 | -------------------------------------------------------------------------------- /internal/app/provider/sql_test.go: -------------------------------------------------------------------------------- 1 | package provider_test 2 | 3 | import ( 4 | "context" 5 | "github.com/cafebazaar/go-boilerplate/internal/app/provider" 6 | "io" 7 | "testing" 8 | 9 | "github.com/cafebazaar/go-boilerplate/pkg/postview" 10 | "github.com/cafebazaar/go-boilerplate/pkg/sql" 11 | "github.com/stretchr/testify/suite" 12 | "golang.org/x/xerrors" 13 | 14 | _ "github.com/jinzhu/gorm/dialects/sqlite" 15 | ) 16 | 17 | type SQLProviderTestSuite struct { 18 | suite.Suite 19 | 20 | provider provider.PostProvider 21 | } 22 | 23 | func TestSQLProviderTestSuite(t *testing.T) { 24 | suite.Run(t, new(SQLProviderTestSuite)) 25 | } 26 | 27 | func (s *SQLProviderTestSuite) TestGetPostShouldReturnNotFoundInitially() { 28 | _, err := s.provider.GetPost(context.Background(), "myToken") 29 | s.True(xerrors.Is(err, provider.ErrNotFound)) 30 | } 31 | 32 | func (s *SQLProviderTestSuite) TestShouldReturnPostAfterAdd() { 33 | err := s.provider.AddPost(context.Background(), &postview.Post{ 34 | Token: "abcd", 35 | Title: "a title", 36 | }) 37 | s.Nil(err) 38 | if err != nil { 39 | return 40 | } 41 | 42 | post, err := s.provider.GetPost(context.Background(), "abcd") 43 | s.Nil(err) 44 | if err != nil { 45 | return 46 | } 47 | 48 | s.Equal("abcd", post.Token) 49 | s.Equal("a title", post.Title) 50 | } 51 | 52 | func (s *SQLProviderTestSuite) SetupTest() { 53 | db, err := sql.GetDatabase(sql.SqliteConfig{ 54 | InMemory: true, 55 | }) 56 | if err != nil { 57 | s.FailNow(err.Error(), "unable to instantiate SQLite instance") 58 | return 59 | } 60 | 61 | s.provider = provider.NewSQL(db) 62 | 63 | err = s.provider.(sql.Migrater).Migrate() 64 | if err != nil { 65 | s.FailNow(err.Error(), "unable to migrate SQLite database") 66 | } 67 | } 68 | 69 | func (s *SQLProviderTestSuite) TearDownTest() { 70 | err := s.provider.(io.Closer).Close() 71 | if err != nil { 72 | s.FailNow(err.Error(), "unable to close SQLite database") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | All contributions are welcome, just follow our contribution guide :) 4 | 5 | If you need a new feature in our code base, simply write your feature and send a merge request. It's encouraged to talk with the system owner beforehand. 6 | 7 | The whole codebase is dockerized so you should be able to run the codebase easily simply using docker. For more information about how to run please read `README.md` 8 | 9 | # Development 10 | 1. All the (code) parts that you've changed or added must have tests. 11 | 2. Test coverage should not decrease by merging your code. 12 | 3. Commit messages should be self explanatory. It should answer following questions: 13 | * Why is this change necessary? 14 | * How does it address the issue? 15 | * What side effects does this change have? 16 | 4. None of your (final) commits should break CI tests. 17 | * There are some minor exceptions but believe me, there's a high probability that your case is not one of them! 18 | 5. Your (final) branch should have just the right number of commits not too many, not too few. 19 | * Logically relevant changes SHOULD get committed together. 20 | * Logically irrelevant changes SHOULD NOT get committed together. 21 | 22 | # Merge Request Process 23 | 1. Rebase your code to current master's head (Do not merge master into your branch, this ruins your branch's history). 24 | 2. Push your branch upstream and create a merge request. 25 | 3. Keep in mind that all merge request must satisfy following criteria to get merged: 26 | * You should test your code vigorously. It's assumed that your merge request does not break anything and works the way it's supposed to. 27 | * Merging your merge request should not decrease test coverage. 28 | * If you're making a change that needs a new side service (like redis) or changes `config.template.json` you SHOULD mention it in your merge request. 29 | 4. Please resolve merge request's issues in less than 7 working days. Keep in mind that inactive merge requests (inactive for more than 14 days) will be closed. 30 | -------------------------------------------------------------------------------- /pkg/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/xerrors" 7 | 8 | "github.com/getsentry/raven-go" 9 | ) 10 | 11 | type wrappedError struct { 12 | message string 13 | next error 14 | frame xerrors.Frame 15 | stackTrace *raven.Stacktrace 16 | extra map[string]interface{} 17 | } 18 | 19 | func New(message string) error { 20 | return makeExtendedError(message, nil, nil) 21 | } 22 | 23 | func NewWithExtra(message string, extra map[string]interface{}) error { 24 | return makeExtendedError(message, nil, extra) 25 | } 26 | 27 | func Wrap(err error, message string) error { 28 | return makeExtendedError(message, err, nil) 29 | } 30 | 31 | func WrapWithExtra(err error, message string, extra map[string]interface{}) error { 32 | return makeExtendedError(message, err, extra) 33 | } 34 | 35 | func Extras(err error) map[string]interface{} { 36 | var wrappedErrorInstance *wrappedError 37 | if xerrors.As(err, &wrappedErrorInstance) { 38 | return wrappedErrorInstance.extra 39 | } 40 | return nil 41 | } 42 | 43 | func makeExtendedError(message string, next error, extra map[string]interface{}) error { 44 | return &wrappedError{ 45 | message: message, 46 | next: next, 47 | frame: xerrors.Caller(2), 48 | stackTrace: raven.GetOrNewStacktrace(next, 2, 0, nil), 49 | extra: mergeMaps(Extras(next), extra), 50 | } 51 | } 52 | 53 | func mergeMaps(maps ...map[string]interface{}) map[string]interface{} { 54 | result := make(map[string]interface{}) 55 | for _, mapInstance := range maps { 56 | for key, value := range mapInstance { 57 | result[key] = value 58 | } 59 | } 60 | 61 | return result 62 | } 63 | 64 | func (err *wrappedError) Error() string { 65 | return fmt.Sprint(err) 66 | } 67 | 68 | // GetStacktrace implements "github.com/evalphobia/logrus_sentry.Stacktracer" 69 | func (err *wrappedError) GetStacktrace() *raven.Stacktrace { 70 | return err.stackTrace 71 | } 72 | 73 | // Unwrap implements "golang.org/x/xerrors.Wrapper" 74 | func (err *wrappedError) Unwrap() error { 75 | return err.next 76 | } 77 | 78 | // Format implements fmt.Formatter 79 | func (err *wrappedError) Format(f fmt.State, c rune) { 80 | xerrors.FormatError(err, f, c) 81 | } 82 | 83 | // FormatError implements xerrors.Formatter 84 | func (err *wrappedError) FormatError(p xerrors.Printer) (next error) { 85 | p.Print(err.message) 86 | err.frame.Format(p) 87 | return err.next 88 | } 89 | -------------------------------------------------------------------------------- /internal/pkg/metrics/prometheus/prometheus_test.go: -------------------------------------------------------------------------------- 1 | package prometheus_test 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/prometheus/client_golang/prometheus/promhttp" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func waitForMetric(t *testing.T, metric string) { 15 | s := assert.New(t) 16 | 17 | hasAppeared := func() bool { 18 | return len(filterOnMetric(t, metric)) > 0 19 | } 20 | 21 | if hasAppeared() { 22 | return 23 | } 24 | 25 | timeout := time.NewTicker(5 * time.Second) 26 | defer timeout.Stop() 27 | 28 | interval := time.NewTicker(100 * time.Millisecond) 29 | defer interval.Stop() 30 | 31 | for { 32 | select { 33 | case <-timeout.C: 34 | s.FailNowf("metric did not appear", 35 | "metric %s did not appear on prometheus", metric) 36 | return 37 | 38 | case <-interval.C: 39 | if hasAppeared() { 40 | return 41 | } 42 | } 43 | } 44 | } 45 | 46 | func filterOnMetric(t *testing.T, metric string) []string { 47 | var result []string 48 | 49 | for _, line := range strings.Split(dumpPrometheus(t), "\n") { 50 | if strings.Contains(line, metric) { 51 | result = append(result, line) 52 | } 53 | } 54 | 55 | return result 56 | } 57 | 58 | func dumpPrometheus(t *testing.T) string { 59 | s := assert.New(t) 60 | 61 | responseWriter := newFakeResponseWriter() 62 | request := &http.Request{ 63 | Method: "GET", 64 | RequestURI: "/", 65 | } 66 | 67 | promhttp.Handler().ServeHTTP(responseWriter, request) 68 | 69 | if responseWriter.statusCode != 200 { 70 | s.FailNowf("prometheus handler failed", 71 | "expected status code 200 but got %d", responseWriter.statusCode) 72 | 73 | return "" 74 | } 75 | 76 | return responseWriter.body.String() 77 | } 78 | 79 | type fakeResponseWriter struct { 80 | statusCode int 81 | body bytes.Buffer 82 | headers http.Header 83 | } 84 | 85 | func newFakeResponseWriter() *fakeResponseWriter { 86 | return &fakeResponseWriter{ 87 | headers: make(http.Header), 88 | } 89 | } 90 | 91 | func (f *fakeResponseWriter) Header() http.Header { 92 | return f.headers 93 | } 94 | 95 | func (f *fakeResponseWriter) Write(body []byte) (int, error) { 96 | return f.body.Write(body) 97 | } 98 | 99 | func (f *fakeResponseWriter) WriteHeader(statusCode int) { 100 | f.statusCode = statusCode 101 | } 102 | -------------------------------------------------------------------------------- /pkg/errors/errors_test.go: -------------------------------------------------------------------------------- 1 | package errors_test 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "path" 7 | "testing" 8 | 9 | "golang.org/x/xerrors" 10 | 11 | "github.com/evalphobia/logrus_sentry" 12 | 13 | "github.com/cafebazaar/go-boilerplate/pkg/errors" 14 | "github.com/stretchr/testify/suite" 15 | ) 16 | 17 | type ErrorsTestSuite struct { 18 | suite.Suite 19 | } 20 | 21 | func TestErrorsTestSuite(t *testing.T) { 22 | suite.Run(t, new(ErrorsTestSuite)) 23 | } 24 | 25 | func (s *ErrorsTestSuite) TestNewShouldContainMessage() { 26 | err := errors.New("hello") 27 | s.Contains(err.Error(), "hello") 28 | } 29 | 30 | func (s *ErrorsTestSuite) TestNewShouldContainStackTraceInFormat() { 31 | err := errors.New("hello") 32 | str := fmt.Sprintf("%+v", err) 33 | s.Contains(str, "errors_test.go") 34 | } 35 | 36 | func (s *ErrorsTestSuite) TestNewShouldImplementRavenStackTracer() { 37 | err := errors.New("hello") 38 | stackTracer, ok := err.(logrus_sentry.Stacktracer) 39 | s.True(ok) 40 | if !ok { 41 | return 42 | } 43 | 44 | stackTrace := stackTracer.GetStacktrace() 45 | s.True(len(stackTrace.Frames) > 0) 46 | if len(stackTrace.Frames) == 0 { 47 | return 48 | } 49 | 50 | s.Equal("errors_test.go", path.Base(stackTrace.Frames[len(stackTrace.Frames)-1].Filename)) 51 | } 52 | 53 | func (s *ErrorsTestSuite) TestNewShouldContainExtras() { 54 | err := errors.NewWithExtra("hello", map[string]interface{}{ 55 | "key": "value", 56 | }) 57 | 58 | extras := errors.Extras(err) 59 | value, ok := extras["key"] 60 | s.True(ok) 61 | s.Equal("value", value) 62 | } 63 | 64 | func (s *ErrorsTestSuite) TestWrapShouldBeUnwrappable() { 65 | cause := errors.New("cause") 66 | err := errors.Wrap(cause, "another error") 67 | s.Equal(cause, xerrors.Unwrap(err)) 68 | } 69 | 70 | func (s *ErrorsTestSuite) TestWrapShouldBeIsCompatible() { 71 | err := errors.Wrap(io.EOF, "hello") 72 | s.NotEqual(io.EOF, err) 73 | s.True(xerrors.Is(err, io.EOF)) 74 | } 75 | 76 | func (s *ErrorsTestSuite) TestWrapShouldMergeExtras() { 77 | cause := errors.NewWithExtra("cause", map[string]interface{}{ 78 | "key1": "value1", 79 | "key2": "value2", 80 | }) 81 | err := errors.WrapWithExtra(cause, "another err", map[string]interface{}{ 82 | "key2": "anothervalue2", 83 | }) 84 | 85 | extras := errors.Extras(err) 86 | 87 | value1, ok := extras["key1"] 88 | s.True(ok) 89 | s.Equal("value1", value1) 90 | 91 | value2, ok := extras["key2"] 92 | s.True(ok) 93 | s.Equal("anothervalue2", value2) 94 | } 95 | -------------------------------------------------------------------------------- /pkg/cache/middlewares/instrumentation_test.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "context" 5 | "github.com/cafebazaar/go-boilerplate/pkg/cache/adaptors" 6 | "github.com/sirupsen/logrus" 7 | "testing" 8 | 9 | metricsMocks "github.com/cafebazaar/go-boilerplate/internal/pkg/metrics/mocks" 10 | 11 | "github.com/stretchr/testify/mock" 12 | "github.com/stretchr/testify/suite" 13 | ) 14 | 15 | type CacheInstrumentationMiddlewareTestSuite struct { 16 | suite.Suite 17 | } 18 | 19 | func TestCacheInstrumentationMiddlewareTestSuite(t *testing.T) { 20 | suite.Run(t, new(CacheInstrumentationMiddlewareTestSuite)) 21 | } 22 | 23 | func (s *CacheInstrumentationMiddlewareTestSuite) TestGetShouldCountOkIsFalse() { 24 | cache := adaptors.NewSynMapAdaptor(logrus.New()) 25 | 26 | mockedObserver := s.makeObserver(map[string]string{ 27 | "method": "Get", 28 | }) 29 | 30 | hooked := NewInstrumentationMiddleware(cache, mockedObserver) 31 | _, err := hooked.Get(context.Background(), "") 32 | s.NotNil(err) 33 | 34 | mockedObserver.AssertExpectations(s.T()) 35 | } 36 | 37 | func (s *CacheInstrumentationMiddlewareTestSuite) TestGetShouldCountOkIsTrue() { 38 | cache := adaptors.NewSynMapAdaptor(logrus.New()) 39 | err := cache.Set(context.Background(), "", []byte("value")) 40 | s.NoError(err) 41 | 42 | mockedObserver := s.makeObserver(map[string]string{ 43 | "method": "Get", 44 | }) 45 | 46 | hooked := NewInstrumentationMiddleware(cache, mockedObserver) 47 | _, err = hooked.Get(context.Background(), "") 48 | s.Nil(err) 49 | 50 | mockedObserver.AssertExpectations(s.T()) 51 | } 52 | 53 | func (s *CacheInstrumentationMiddlewareTestSuite) makeObserver( 54 | expectedLabels map[string]string) *metricsMocks.Observer { 55 | 56 | mockedObserver := &metricsMocks.Observer{} 57 | mockedObserver.On("Observe", mock.Anything).Once() 58 | mockedObserver.On("With", s.makeMatcher(expectedLabels)).Once().Return(mockedObserver) 59 | 60 | return mockedObserver 61 | } 62 | 63 | func (s *CacheInstrumentationMiddlewareTestSuite) makeMatcher( 64 | expectedLabels map[string]string) interface{} { 65 | 66 | return mock.MatchedBy(func(labels map[string]string) bool { 67 | result := true 68 | 69 | for expectedKey, expectedValue := range expectedLabels { 70 | value, ok := labels[expectedKey] 71 | s.True(ok, "expected to find label %v", expectedKey) 72 | if !ok { 73 | result = false 74 | continue 75 | } 76 | 77 | s.Equal(expectedValue, value, 78 | "expected to find value %v for key %v", expectedValue, expectedKey) 79 | result = result && expectedValue == value 80 | } 81 | 82 | return result 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /internal/app/core/core_test.go: -------------------------------------------------------------------------------- 1 | package core_test 2 | 3 | import ( 4 | "context" 5 | "github.com/cafebazaar/go-boilerplate/internal/app/provider" 6 | "github.com/cafebazaar/go-boilerplate/pkg/cache/adaptors" 7 | "github.com/sirupsen/logrus" 8 | "github.com/stretchr/testify/mock" 9 | "google.golang.org/grpc/codes" 10 | "google.golang.org/grpc/status" 11 | "testing" 12 | 13 | "github.com/cafebazaar/go-boilerplate/internal/app/core" 14 | "github.com/cafebazaar/go-boilerplate/pkg/postview" 15 | "github.com/golang/protobuf/proto" 16 | 17 | providerMocks "github.com/cafebazaar/go-boilerplate/internal/app/provider/mocks" 18 | "github.com/stretchr/testify/suite" 19 | ) 20 | 21 | type CoreTestSuite struct { 22 | suite.Suite 23 | } 24 | 25 | func TestCoreTestSuite(t *testing.T) { 26 | suite.Run(t, new(CoreTestSuite)) 27 | } 28 | 29 | func (s *CoreTestSuite) TestShouldReturnNotFoundIfProviderReturnsNotFound() { 30 | cache := adaptors.NewSynMapAdaptor(logrus.New()) 31 | 32 | mockProvider := &providerMocks.PostProvider{} 33 | mockProvider.On("GetPost", mock.Anything, mock.Anything).Once().Return(nil, provider.ErrNotFound) 34 | 35 | c := core.New(mockProvider, cache) 36 | _, err := c.GetPost(context.Background(), &postview.GetPostRequest{}) 37 | s.NotNil(err) 38 | 39 | grpcStatus, ok := status.FromError(err) 40 | s.True(ok) 41 | if !ok { 42 | return 43 | } 44 | s.Equal(codes.NotFound, grpcStatus.Code()) 45 | } 46 | 47 | func (s *CoreTestSuite) TestShouldReturnFromCacheIfFound() { 48 | cache := adaptors.NewSynMapAdaptor(logrus.New()) 49 | c := core.New(nil, cache) 50 | token := "token" 51 | title := "title" 52 | data, err := proto.Marshal(&postview.Post{ 53 | Token: token, 54 | Title: title, 55 | }) 56 | if !s.NoError(err, "fail to marshalize post") { 57 | return 58 | } 59 | err = cache.Set(context.Background(), token, data) 60 | if !s.NoError(err, "fail to marshalize post") { 61 | return 62 | } 63 | response, err := c.GetPost(context.Background(), &postview.GetPostRequest{ 64 | Token: token, 65 | }) 66 | 67 | s.NoError(err) 68 | s.Equal(response.Post.Token, token) 69 | s.Equal(response.Post.Title, title) 70 | } 71 | 72 | func (s *CoreTestSuite) TestShouldReturnFromProviderIfNotCached() { 73 | cache := adaptors.NewSynMapAdaptor(logrus.New()) 74 | 75 | mockProvider := &providerMocks.PostProvider{} 76 | mockProvider.On("GetPost", mock.Anything, "token").Once().Return(&postview.Post{ 77 | Token: "token", 78 | Title: "title", 79 | }, nil) 80 | 81 | c := core.New(mockProvider, cache) 82 | response, err := c.GetPost(context.Background(), &postview.GetPostRequest{ 83 | Token: "token", 84 | }) 85 | 86 | s.Nil(err) 87 | s.Equal(response.Post.Token, "token") 88 | s.Equal(response.Post.Title, "title") 89 | } 90 | -------------------------------------------------------------------------------- /internal/app/core/core.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cafebazaar/go-boilerplate/pkg/errors" 7 | "golang.org/x/xerrors" 8 | 9 | "github.com/cafebazaar/go-boilerplate/internal/app/provider" 10 | "github.com/cafebazaar/go-boilerplate/pkg/cache" 11 | "github.com/cafebazaar/go-boilerplate/pkg/postview" 12 | "github.com/golang/protobuf/proto" 13 | "github.com/sirupsen/logrus" 14 | "google.golang.org/grpc/codes" 15 | "google.golang.org/grpc/status" 16 | ) 17 | 18 | type core struct { 19 | provider provider.PostProvider 20 | cache cache.Layer 21 | } 22 | 23 | func New(provider provider.PostProvider, cache cache.Layer) postview.PostViewServer { 24 | return &core{ 25 | provider: provider, 26 | cache: cache, 27 | } 28 | } 29 | 30 | func (c *core) GetPost(ctx context.Context, request *postview.GetPostRequest) (*postview.GetPostResponse, error) { 31 | post, err := c.getPostFromCache(ctx, request.Token) 32 | if err != nil { 33 | logrus.WithError(err).WithFields(map[string]interface{}{ 34 | "token": request.Token, 35 | }).Error("failed to load data from cache") 36 | } else { 37 | return &postview.GetPostResponse{ 38 | Post: post, 39 | }, nil 40 | } 41 | 42 | post, err = c.provider.GetPost(ctx, request.Token) 43 | if err != nil { 44 | if xerrors.Is(err, provider.ErrNotFound) { 45 | return nil, status.Error(codes.NotFound, "post not found") 46 | } 47 | 48 | return nil, errors.WrapWithExtra(err, "failed to acquire post", map[string]interface{}{ 49 | "request": request, 50 | }) 51 | } 52 | 53 | err = c.setPostFromCache(ctx, request.Token, post) 54 | if err != nil { 55 | logrus.WithError(err).WithFields(map[string]interface{}{ 56 | "token": request.Token, 57 | }).Error("failed to set data in cache") 58 | } 59 | return &postview.GetPostResponse{ 60 | Post: post, 61 | }, nil 62 | } 63 | 64 | func (c *core) getPostFromCache(ctx context.Context, token string) (*postview.Post, error) { 65 | data, err := c.cache.Get(ctx, token) 66 | if err != nil { 67 | return nil, errors.Wrap(err, "fail to get post from cache") 68 | } 69 | var result = &postview.Post{} 70 | err = proto.Unmarshal(data, result) 71 | if err != nil { 72 | return nil, errors.WrapWithExtra(err, "failed to unmarshal proto", map[string]interface{}{ 73 | "token": token, 74 | }) 75 | } 76 | 77 | logrus.Info("load post from cache") 78 | return result, nil 79 | 80 | } 81 | 82 | func (c *core) setPostFromCache(ctx context.Context, token string, post *postview.Post) (err error) { 83 | data, err := proto.Marshal(post) 84 | if err != nil { 85 | return errors.Wrap(err, "fail to marshal post") 86 | } 87 | err = c.cache.Set(ctx, token, data) 88 | if err != nil { 89 | return errors.Wrap(err, "fail to set post in cache") 90 | } 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /internal/pkg/metrics/prometheus/histogram_test.go: -------------------------------------------------------------------------------- 1 | package prometheus_test 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/cafebazaar/go-boilerplate/internal/pkg/metrics" 9 | "github.com/cafebazaar/go-boilerplate/internal/pkg/metrics/prometheus" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type HistogramTestSuite struct { 14 | suite.Suite 15 | 16 | sampleMetric metrics.Observer 17 | sampleMetricWithLabel metrics.Observer 18 | } 19 | 20 | func TestHistogramTestSuite(t *testing.T) { 21 | suite.Run(t, new(HistogramTestSuite)) 22 | } 23 | 24 | func (s *HistogramTestSuite) TestShouldContainType() { 25 | s.True(strings.Contains(dumpPrometheus(s.T()), "# TYPE sample_histogram histogram")) 26 | } 27 | 28 | func (s *HistogramTestSuite) TestShouldContainHelp() { 29 | s.True(strings.Contains(dumpPrometheus(s.T()), "# HELP sample_histogram a sample metric")) 30 | } 31 | 32 | func (s *HistogramTestSuite) TestShouldCount() { 33 | s.True(strings.Contains(dumpPrometheus(s.T()), "sample_histogram_count 2")) 34 | } 35 | 36 | func (s *HistogramTestSuite) TestShouldSum() { 37 | s.True(strings.Contains(dumpPrometheus(s.T()), "sample_histogram_sum 3")) 38 | } 39 | 40 | func (s *HistogramTestSuite) TestShouldApplyLabels() { 41 | s.True(strings.Contains(dumpPrometheus(s.T()), "sample_histogram_with_label_count{my_label=\"my_value\"} 1")) 42 | } 43 | 44 | func (s *HistogramTestSuite) TestShouldNotHaveDataRaceOnConcurrentAccessWithLabel() { 45 | const NumberOfGoRoutines = 10 46 | 47 | metric := prometheus.NewHistogram("concurrent_histogram_metric_with_label", "a sample metric", "my_label") 48 | var started sync.WaitGroup 49 | var beginOperating sync.WaitGroup 50 | var testFinished sync.WaitGroup 51 | 52 | started.Add(NumberOfGoRoutines) 53 | testFinished.Add(NumberOfGoRoutines) 54 | beginOperating.Add(1) 55 | 56 | for i := 0; i < NumberOfGoRoutines; i++ { 57 | go func() { 58 | defer testFinished.Done() 59 | 60 | started.Done() 61 | beginOperating.Wait() 62 | 63 | for j := 0; j < 1000; j++ { 64 | metric.With(map[string]string{ 65 | "my_label": "my_value", 66 | }).Observe(1.0) 67 | } 68 | }() 69 | } 70 | 71 | started.Wait() 72 | beginOperating.Done() 73 | 74 | testFinished.Wait() 75 | } 76 | 77 | func (s *HistogramTestSuite) SetupSuite() { 78 | s.sampleMetricWithLabel = prometheus.NewHistogram("sample_histogram_with_label", "a sample metric", "my_label") 79 | s.sampleMetric = prometheus.NewHistogram("sample_histogram", "a sample metric") 80 | 81 | s.sampleMetric.Observe(1.0) 82 | s.sampleMetric.Observe(2.0) 83 | waitForMetric(s.T(), "sample_histogram") 84 | 85 | s.sampleMetricWithLabel.With(map[string]string{ 86 | "my_label": "my_value", 87 | }).Observe(1.0) 88 | waitForMetric(s.T(), "sample_histogram_with_label") 89 | } 90 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: registry.cafebazaar.ir:5000/common-images/builder:docker-kube 2 | 3 | variables: 4 | BUILD_IMAGE_TAG: $CI_REGISTRY_IMAGE:ci_build__${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA} 5 | BASE_IMAGE_TAG: $CI_REGISTRY_IMAGE:ci_base__${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA} 6 | 7 | stages: 8 | - build 9 | - test 10 | - push 11 | - deploy 12 | - cleanup 13 | 14 | build: 15 | stage: build 16 | script: 17 | - docker build . -f Dockerfile.base -t $BASE_IMAGE_TAG --build-arg http_proxy=$http_proxy --build-arg HTTP_PROXY=$HTTP_PROXY --build-arg https_proxy=$https_proxy --build-arg HTTPS_PROXY=$HTTPS_PROXY --build-arg no_proxy=$no_proxy --build-arg NO_PROXY=$NO_PROXY 18 | - sed -i "s~BASE_IMAGE_TAG~$BASE_IMAGE_TAG~g" Dockerfile.build 19 | - docker build . -f Dockerfile.build -t $BUILD_IMAGE_TAG --build-arg CI_JOB_TOKEN=$CI_JOB_TOKEN --build-arg http_proxy=$http_proxy --build-arg HTTP_PROXY=$HTTP_PROXY --build-arg https_proxy=$https_proxy --build-arg HTTPS_PROXY=$HTTPS_PROXY --build-arg no_proxy=$no_proxy --build-arg NO_PROXY=$NO_PROXY 20 | 21 | lint: 22 | stage: test 23 | except: 24 | - master 25 | - tags 26 | script: 27 | - docker run -t --rm $BUILD_IMAGE_TAG make lint 28 | 29 | test: 30 | stage: test 31 | except: 32 | - master 33 | - tags 34 | script: 35 | - docker run -t -e MULTILAYERCACHE_REDIS_ADDR=redis:6379 --rm $BUILD_IMAGE_TAG make check 36 | services: 37 | - redis:latest 38 | 39 | race: 40 | stage: test 41 | except: 42 | - master 43 | - tags 44 | script: 45 | - docker run -t -e MULTILAYERCACHE_REDIS_ADDR=redis:6379 --rm $BUILD_IMAGE_TAG make race 46 | services: 47 | - redis:latest 48 | 49 | coverage: 50 | stage: test 51 | script: 52 | - docker run -t -e MULTILAYERCACHE_REDIS_ADDR=redis:6379 --rm $BUILD_IMAGE_TAG make coverage 53 | except: 54 | - tags 55 | coverage: '/Total Coverage:\s+(\d+.\d+\%)/' 56 | services: 57 | - redis:latest 58 | 59 | push: 60 | stage: push 61 | only: 62 | - tags 63 | services: 64 | - docker:dind 65 | script: 66 | - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY 67 | - sed -i "s~BUILD_IMAGE_TAG~$BUILD_IMAGE_TAG~g" Dockerfile 68 | - make docker 69 | - make push 70 | 71 | deploy: 72 | stage: deploy 73 | when: manual 74 | only: 75 | - tags 76 | environment: 77 | name: prod 78 | allow_failure: false 79 | script: 80 | - kubectl config set-cluster cafecluster --server=$KUBE_URL --insecure-skip-tls-verify=true 81 | - kubectl config set-credentials divar-infra --token=$KUBE_TOKEN 82 | - kubectl config set-context default-context --cluster=cafecluster --user=divar-infra 83 | - kubectl config use-context default-context 84 | - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY 85 | - make push-production 86 | - make deploy 87 | 88 | cleanup: 89 | stage: cleanup 90 | when: always 91 | script: 92 | - docker rmi $BUILD_IMAGE_TAG 93 | -------------------------------------------------------------------------------- /pkg/cache/multilayercache/multilayercache.go: -------------------------------------------------------------------------------- 1 | package multilayercache 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/cafebazaar/go-boilerplate/pkg/cache" 8 | "github.com/pkg/errors" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type multilayerCache struct { 13 | layers []cache.Layer 14 | } 15 | 16 | type layerOperator func(layer cache.Layer) (interface{}, error) 17 | 18 | func New(layers ...cache.Layer) cache.Layer { 19 | return &multilayerCache{ 20 | layers: layers, 21 | } 22 | 23 | } 24 | 25 | func (mc *multilayerCache) Get(ctx context.Context, key string) ([]byte, error) { 26 | result, err := mc.performOperation(func(layer cache.Layer) (interface{}, error) { 27 | return layer.Get(ctx, key) 28 | }) 29 | 30 | if err != nil { 31 | return nil, err 32 | } 33 | return result.([]byte), nil 34 | } 35 | 36 | func (mc *multilayerCache) Delete(ctx context.Context, key string) error { 37 | _, err := mc.performOperation(func(layer cache.Layer) (interface{}, error) { 38 | err := layer.Delete(ctx, key) 39 | return nil, err 40 | }) 41 | 42 | return err 43 | } 44 | 45 | func (mc *multilayerCache) Set(ctx context.Context, key string, value []byte) error { 46 | _, err := mc.performOperation(func(layer cache.Layer) (interface{}, error) { 47 | err := layer.Set(ctx, key, value) 48 | return nil, err 49 | }) 50 | 51 | return err 52 | } 53 | 54 | func (mc *multilayerCache) Clear(ctx context.Context) error { 55 | _, err := mc.performOperation(func(layer cache.Layer) (interface{}, error) { 56 | err := layer.Clear(ctx) 57 | return nil, err 58 | }) 59 | 60 | return err 61 | } 62 | 63 | func (mc *multilayerCache) wrapAllErrors(errChannel <-chan error) error { 64 | var allErrors error 65 | for err := range errChannel { 66 | if allErrors == nil { 67 | allErrors = err 68 | } else { 69 | allErrors = errors.Wrap(err, allErrors.Error()) 70 | } 71 | } 72 | 73 | return allErrors 74 | } 75 | 76 | func (mc *multilayerCache) performOperation(operator layerOperator) (interface{}, error) { 77 | errChannel := make(chan error, len(mc.layers)) 78 | resultChannel := make(chan interface{}) 79 | 80 | var wg sync.WaitGroup 81 | wg.Add(len(mc.layers)) 82 | 83 | done := make(chan struct{}) 84 | go func() { 85 | wg.Wait() 86 | close(done) 87 | close(errChannel) 88 | close(resultChannel) 89 | }() 90 | 91 | for _, cacheLayer := range mc.layers { 92 | go func(layer cache.Layer) { 93 | defer wg.Done() 94 | 95 | value, err := operator(layer) 96 | if err != nil { 97 | errChannel <- err 98 | return 99 | } 100 | 101 | select { 102 | case resultChannel <- value: 103 | default: 104 | } 105 | }(cacheLayer) 106 | } 107 | 108 | select { 109 | case value := <-resultChannel: 110 | go func() { 111 | for err := range errChannel { 112 | logrus.WithError(err).Error("failed to get value from multilayer cache") 113 | } 114 | }() 115 | 116 | return value, nil 117 | 118 | case <-done: 119 | return nil, mc.wrapAllErrors(errChannel) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /internal/app/provider/sql.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | 7 | "github.com/cafebazaar/go-boilerplate/pkg/errors" 8 | "github.com/golang/protobuf/proto" 9 | 10 | "github.com/cafebazaar/go-boilerplate/pkg/postview" 11 | "gopkg.in/gormigrate.v1" 12 | 13 | "github.com/jinzhu/gorm" 14 | ) 15 | 16 | type sqlProvider struct { 17 | db *gorm.DB 18 | } 19 | 20 | type post struct { 21 | gorm.Model 22 | 23 | Token string `gorm:"type:varchar(8);primary_key;"` 24 | Data string `gorm:"type:text;"` 25 | } 26 | 27 | func NewSQL(db *gorm.DB) PostProvider { 28 | return sqlProvider{ 29 | db: db, 30 | } 31 | } 32 | 33 | func (p sqlProvider) Close() error { 34 | return p.db.Close() 35 | } 36 | 37 | func (p sqlProvider) GetPost(ctx context.Context, token string) (*postview.Post, error) { 38 | postInstance := &post{} 39 | err := p.db.Where("token = ?", token).First(postInstance).Error 40 | if err != nil { 41 | if gorm.IsRecordNotFoundError(err) { 42 | return nil, errors.WrapWithExtra(ErrNotFound, "post not found", map[string]interface{}{ 43 | "token": token, 44 | }) 45 | } 46 | return nil, errors.WrapWithExtra(err, "could not read post from db", map[string]interface{}{ 47 | "token": token, 48 | }) 49 | } 50 | 51 | result, err := p.modelToProto(postInstance) 52 | if err != nil { 53 | return nil, errors.WrapWithExtra(err, "could not convert model to proto", map[string]interface{}{ 54 | "token": token, 55 | }) 56 | } 57 | 58 | return result, nil 59 | } 60 | 61 | func (p sqlProvider) AddPost(ctx context.Context, protoPost *postview.Post) error { 62 | modelInstance, err := p.protoToModel(protoPost) 63 | if err != nil { 64 | return errors.WrapWithExtra(err, "could not convert proto to model", map[string]interface{}{ 65 | "post": protoPost, 66 | }) 67 | } 68 | 69 | err = p.db.Create(modelInstance).Error 70 | if err != nil { 71 | return errors.WrapWithExtra(err, "could not add model to db", map[string]interface{}{ 72 | "post": protoPost, 73 | }) 74 | } 75 | 76 | return nil 77 | } 78 | 79 | func (p sqlProvider) Migrate() error { 80 | m := gormigrate.New(p.db, gormigrate.DefaultOptions, []*gormigrate.Migration{ 81 | // create model table 82 | { 83 | ID: "201906082327", 84 | Migrate: func(tx *gorm.DB) error { 85 | type post struct { 86 | gorm.Model 87 | 88 | Token string `gorm:"type:varchar(8);primary_key;"` 89 | Data string `gorm:"type:text;"` 90 | } 91 | return tx.AutoMigrate(&post{}).Error 92 | }, 93 | Rollback: func(tx *gorm.DB) error { 94 | return tx.DropTable("posts").Error 95 | }, 96 | }, 97 | }) 98 | 99 | return m.Migrate() 100 | } 101 | 102 | func (p sqlProvider) protoToModel(protoPost *postview.Post) (*post, error) { 103 | binaryData, err := proto.Marshal(protoPost) 104 | if err != nil { 105 | return nil, errors.Wrap(err, "could not marshal proto") 106 | } 107 | 108 | data := base64.StdEncoding.EncodeToString(binaryData) 109 | 110 | return &post{ 111 | Token: protoPost.Token, 112 | Data: data, 113 | }, nil 114 | } 115 | 116 | func (p sqlProvider) modelToProto(m *post) (*postview.Post, error) { 117 | data, err := base64.StdEncoding.DecodeString(m.Data) 118 | if err != nil { 119 | return nil, errors.Wrap(err, "could not decode base64") 120 | } 121 | 122 | var result postview.Post 123 | err = proto.Unmarshal(data, &result) 124 | if err != nil { 125 | return nil, errors.Wrap(err, "could not unmarshal proto") 126 | } 127 | 128 | return &result, nil 129 | } 130 | -------------------------------------------------------------------------------- /cmd/postviewd/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/cafebazaar/go-boilerplate/pkg/errors" 8 | 9 | "github.com/cafebazaar/go-boilerplate/pkg/sql" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | 13 | _ "github.com/jinzhu/gorm/dialects/postgres" 14 | ) 15 | 16 | // Config the application's configuration structure 17 | type Config struct { 18 | Logging LoggingConfig 19 | ConfigFile string 20 | ListenPort int 21 | MetricListenPort int 22 | Database sql.PostgresConfig 23 | Cache CacheConfig 24 | } 25 | 26 | type CacheConfig struct { 27 | Redis RedisConfig 28 | BigCache BigCacheConfig 29 | } 30 | 31 | type RedisConfig struct { 32 | Enabled bool 33 | Host string 34 | Port int 35 | DB int 36 | Prefix string 37 | ExpirationTime time.Duration 38 | } 39 | 40 | type BigCacheConfig struct { 41 | Enabled bool 42 | ExpirationTime time.Duration 43 | MaxSpace int 44 | Shards int 45 | LifeWindow time.Duration 46 | MaxEntriesInWindow int 47 | MaxEntrySize int 48 | Verbose bool 49 | HardMaxCacheSize int 50 | } 51 | 52 | // LoadConfig loads the config from a file if specified, otherwise from the environment 53 | func LoadConfig(cmd *cobra.Command) (*Config, error) { 54 | // Setting defaults for this application 55 | viper.SetDefault("logging.SentryEnabled", false) 56 | viper.SetDefault("logging.level", "error") 57 | viper.SetDefault("listenPort", 8080) 58 | viper.SetDefault("metricListenPort", 8081) 59 | viper.SetDefault("database.host", "127.0.0.1") 60 | viper.SetDefault("database.port", 5432) 61 | viper.SetDefault("database.username", "postgres") 62 | viper.SetDefault("database.password", "") 63 | viper.SetDefault("database.database", "postview") 64 | viper.SetDefault("database.ssl", false) 65 | viper.SetDefault("database.maxIdleConnection", 0) 66 | viper.SetDefault("database.maxOpenConnection", 0) 67 | viper.SetDefault("cache.redis.host", "127.0.0.1") 68 | viper.SetDefault("cache.redis.port", 6379) 69 | viper.SetDefault("cache.redis.db", 0) 70 | viper.SetDefault("cache.redis.expirationTime", 3*time.Hour) 71 | viper.SetDefault("cache.redis.prefix", "POST_VIEW") 72 | viper.SetDefault("cache.bigCache.shards", 1024) 73 | viper.SetDefault("cache.bigCache.maxEntriesInWindow", 1100*10*60) 74 | viper.SetDefault("cache.bigCache.maxEntrySize", 500) 75 | viper.SetDefault("cache.bigCache.verbose", true) 76 | viper.SetDefault("cache.bigCache.hardMaxCacheSize", 125) 77 | 78 | // Read Config from ENV 79 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 80 | viper.SetEnvPrefix("POSTVIEW") 81 | viper.AutomaticEnv() 82 | 83 | // Read Config from Flags 84 | err := viper.BindPFlags(cmd.Flags()) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | // Read Config from file 90 | if configFile, err := cmd.Flags().GetString("config-file"); err == nil && configFile != "" { 91 | viper.SetConfigFile(configFile) 92 | 93 | if err := viper.ReadInConfig(); err != nil { 94 | return nil, err 95 | } 96 | } 97 | 98 | var config Config 99 | 100 | err = viper.Unmarshal(&config) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | return &config, nil 106 | } 107 | 108 | func provideConfig(cmd *cobra.Command) (*Config, error) { 109 | config, err := LoadConfig(cmd) 110 | if err != nil { 111 | return nil, errors.Wrap(err, "Failed to load configurations.") 112 | } 113 | return config, nil 114 | } 115 | -------------------------------------------------------------------------------- /internal/app/provider/instrumentation_middleware_test.go: -------------------------------------------------------------------------------- 1 | package provider_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/cafebazaar/go-boilerplate/internal/app/provider" 7 | "testing" 8 | 9 | providerMocks "github.com/cafebazaar/go-boilerplate/internal/app/provider/mocks" 10 | metricsMocks "github.com/cafebazaar/go-boilerplate/internal/pkg/metrics/mocks" 11 | 12 | "github.com/stretchr/testify/mock" 13 | "github.com/stretchr/testify/suite" 14 | ) 15 | 16 | type ProviderInstrumentationMiddlewareTestSuite struct { 17 | suite.Suite 18 | } 19 | 20 | func TestProviderInstrumentationMiddlewareTestSuite(t *testing.T) { 21 | suite.Run(t, new(ProviderInstrumentationMiddlewareTestSuite)) 22 | } 23 | 24 | func (s *ProviderInstrumentationMiddlewareTestSuite) TestGetPostShouldCountErrIsNil() { 25 | mockedProvider := &providerMocks.PostProvider{} 26 | mockedProvider.On("GetPost", mock.Anything, mock.Anything).Return(nil, nil) 27 | 28 | mockedObserver := s.makeObserver(map[string]string{ 29 | "method": "GetPost", 30 | "success": "true", 31 | }) 32 | 33 | hooked := provider.NewInstrumentationMiddleware(mockedProvider, mockedObserver) 34 | _, err := hooked.GetPost(context.Background(), "") 35 | s.Nil(err) 36 | 37 | mockedObserver.AssertExpectations(s.T()) 38 | mockedProvider.AssertExpectations(s.T()) 39 | } 40 | 41 | func (s *ProviderInstrumentationMiddlewareTestSuite) TestGetPostShouldCountErrIsNotNil() { 42 | mockedProvider := &providerMocks.PostProvider{} 43 | mockedProvider.On("GetPost", mock.Anything, mock.Anything).Return(nil, errors.New("some err")) 44 | 45 | mockedObserver := s.makeObserver(map[string]string{ 46 | "method": "GetPost", 47 | "success": "false", 48 | }) 49 | 50 | hooked := provider.NewInstrumentationMiddleware(mockedProvider, mockedObserver) 51 | _, err := hooked.GetPost(context.Background(), "") 52 | s.NotNil(err) 53 | 54 | mockedObserver.AssertExpectations(s.T()) 55 | mockedProvider.AssertExpectations(s.T()) 56 | } 57 | 58 | func (s *ProviderInstrumentationMiddlewareTestSuite) TestAddPostShouldCountErrIsNil() { 59 | mockedProvider := &providerMocks.PostProvider{} 60 | mockedProvider.On("AddPost", mock.Anything, mock.Anything).Return(nil) 61 | 62 | mockedObserver := s.makeObserver(map[string]string{ 63 | "method": "AddPost", 64 | "success": "true", 65 | }) 66 | 67 | hooked := provider.NewInstrumentationMiddleware(mockedProvider, mockedObserver) 68 | err := hooked.AddPost(context.Background(), nil) 69 | s.Nil(err) 70 | 71 | mockedObserver.AssertExpectations(s.T()) 72 | mockedProvider.AssertExpectations(s.T()) 73 | } 74 | 75 | func (s *ProviderInstrumentationMiddlewareTestSuite) TestAddPostShouldCountErrIsNotNil() { 76 | mockedProvider := &providerMocks.PostProvider{} 77 | mockedProvider.On("AddPost", mock.Anything, mock.Anything).Return(errors.New("some err")) 78 | 79 | mockedObserver := s.makeObserver(map[string]string{ 80 | "method": "AddPost", 81 | "success": "false", 82 | }) 83 | 84 | hooked := provider.NewInstrumentationMiddleware(mockedProvider, mockedObserver) 85 | err := hooked.AddPost(context.Background(), nil) 86 | s.NotNil(err) 87 | 88 | mockedObserver.AssertExpectations(s.T()) 89 | mockedProvider.AssertExpectations(s.T()) 90 | } 91 | 92 | func (s *ProviderInstrumentationMiddlewareTestSuite) makeObserver( 93 | expectedLabels map[string]string) *metricsMocks.Observer { 94 | 95 | mockedObserver := &metricsMocks.Observer{} 96 | mockedObserver.On("Observe", mock.Anything).Once() 97 | mockedObserver.On("With", s.makeMatcher(expectedLabels)).Once().Return(mockedObserver) 98 | 99 | return mockedObserver 100 | } 101 | 102 | func (s *ProviderInstrumentationMiddlewareTestSuite) makeMatcher( 103 | expectedLabels map[string]string) interface{} { 104 | 105 | return mock.MatchedBy(func(labels map[string]string) bool { 106 | result := true 107 | 108 | for expectedKey, expectedValue := range expectedLabels { 109 | value, ok := labels[expectedKey] 110 | s.True(ok, "expected to find label %v", expectedKey) 111 | if !ok { 112 | result = false 113 | continue 114 | } 115 | 116 | s.Equal(expectedValue, value, 117 | "expected to find value %v for key %v", expectedValue, expectedKey) 118 | result = result && expectedValue == value 119 | } 120 | 121 | return result 122 | }) 123 | } 124 | -------------------------------------------------------------------------------- /cmd/postviewd/serve.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/cafebazaar/go-boilerplate/internal/app/provider" 7 | "github.com/cafebazaar/go-boilerplate/internal/pkg/grpcserver" 8 | "github.com/cafebazaar/go-boilerplate/internal/pkg/metrics/prometheus" 9 | "log" 10 | "os" 11 | "os/signal" 12 | "sync" 13 | "syscall" 14 | 15 | "github.com/allegro/bigcache" 16 | "github.com/cafebazaar/go-boilerplate/pkg/cache" 17 | "github.com/cafebazaar/go-boilerplate/pkg/cache/adaptors" 18 | "github.com/cafebazaar/go-boilerplate/pkg/cache/middlewares" 19 | "github.com/cafebazaar/go-boilerplate/pkg/cache/multilayercache" 20 | "github.com/cafebazaar/go-boilerplate/pkg/errors" 21 | "github.com/cafebazaar/go-boilerplate/pkg/postview" 22 | 23 | "github.com/go-redis/redis" 24 | "github.com/sirupsen/logrus" 25 | "github.com/spf13/cobra" 26 | 27 | "github.com/cafebazaar/go-boilerplate/pkg/sql" 28 | ) 29 | 30 | var serveCmd = &cobra.Command{ 31 | Use: "serve", 32 | Short: "start Server", 33 | Run: serve, 34 | } 35 | 36 | func init() { 37 | rootCmd.AddCommand(serveCmd) 38 | } 39 | 40 | func serve(cmd *cobra.Command, args []string) { 41 | printVersion() 42 | 43 | serverCtx, serverCancel := makeServerCtx() 44 | defer serverCancel() 45 | 46 | server, err := CreateServer(serverCtx, cmd) 47 | panicWithError(err, "failed to create server") 48 | 49 | var serverWaitGroup sync.WaitGroup 50 | 51 | serverWaitGroup.Add(1) 52 | go func() { 53 | defer serverWaitGroup.Done() 54 | 55 | if err := server.Serve(); err != nil { 56 | panicWithError(err, "failed to serve") 57 | } 58 | }() 59 | 60 | if err := declareReadiness(); err != nil { 61 | log.Fatal(err) 62 | } 63 | 64 | <-serverCtx.Done() 65 | 66 | server.Stop() 67 | 68 | serverWaitGroup.Wait() 69 | } 70 | 71 | func provideServer(server postview.PostViewServer, config *Config, logger *logrus.Logger) (*grpcserver.Server, error) { 72 | return grpcserver.New(server, logger, config.ListenPort) 73 | } 74 | 75 | func provideProvider(config *Config, logger *logrus.Logger, prometheusMetric *prometheus.Server) provider.PostProvider { 76 | db, err := sql.GetDatabase(config.Database) 77 | if err != nil { 78 | logrus.WithError(err).WithField( 79 | "database", config.Database).Panic("failed to connect to DB") 80 | return nil 81 | } 82 | 83 | providerInstance := provider.NewSQL(db) 84 | providerInstance = provider.NewInstrumentationMiddleware( 85 | providerInstance, postProviderMetrics.With(map[string]string{ 86 | "provider_type": "postgres", 87 | })) 88 | 89 | return providerInstance 90 | } 91 | 92 | func provideCache(config *Config, prometheusMetric *prometheus.Server) (cache.Layer, error) { 93 | var cacheLayers []cache.Layer 94 | if config.Cache.Redis.Enabled { 95 | redisClient := redis.NewClient(&redis.Options{ 96 | Addr: fmt.Sprintf("%s:%d", config.Cache.Redis.Host, config.Cache.Redis.Port), 97 | DB: config.Cache.Redis.DB, 98 | }) 99 | // Ping Redis 100 | err := redisClient.Ping().Err() 101 | if err != nil { 102 | return nil, errors.Wrap(err, "fail to connect to redis") 103 | } 104 | cacheLayers = append(cacheLayers, adaptors.NewRedisAdaptor(config.Cache.Redis.ExpirationTime, redisClient)) 105 | 106 | } 107 | 108 | if config.Cache.BigCache.Enabled { 109 | bigCacheInstance, err := bigcache.NewBigCache(bigcache.Config{ 110 | Shards: config.Cache.BigCache.Shards, 111 | LifeWindow: config.Cache.BigCache.ExpirationTime, 112 | MaxEntriesInWindow: config.Cache.BigCache.MaxEntriesInWindow, 113 | MaxEntrySize: config.Cache.BigCache.MaxEntrySize, 114 | Verbose: config.Cache.BigCache.Verbose, 115 | HardMaxCacheSize: config.Cache.BigCache.HardMaxCacheSize, 116 | }) 117 | if err != nil { 118 | return nil, errors.Wrap(err, "fail to initialize big cache") 119 | } 120 | 121 | cacheLayers = append(cacheLayers, adaptors.NewBigCacheAdaptor(bigCacheInstance)) 122 | 123 | } 124 | 125 | cacheInstance := multilayercache.New(cacheLayers...) 126 | 127 | cacheInstance = middlewares.NewInstrumentationMiddleware( 128 | cacheInstance, cacheMetrics.With(map[string]string{ 129 | "cache_type": "multilayer", 130 | })) 131 | 132 | return cacheInstance, nil 133 | } 134 | 135 | func makeServerCtx() (context.Context, context.CancelFunc) { 136 | gracefulStop := make(chan os.Signal) 137 | 138 | signal.Notify(gracefulStop, syscall.SIGTERM) 139 | signal.Notify(gracefulStop, syscall.SIGINT) 140 | 141 | ctx, cancel := context.WithCancel(context.Background()) 142 | 143 | go func() { 144 | <-gracefulStop 145 | cancel() 146 | }() 147 | 148 | return ctx, cancel 149 | } 150 | 151 | func declareReadiness() error { 152 | // nolint: gosec 153 | file, err := os.Create("/tmp/readiness") 154 | if err != nil { 155 | return err 156 | } 157 | // nolint: errcheck 158 | defer file.Close() 159 | 160 | _, err = file.WriteString("ready") 161 | return err 162 | } 163 | 164 | func panicWithError(err error, format string, args ...interface{}) { 165 | logrus.WithError(err).Panicf(format, args...) 166 | } 167 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help generate lint fmt dependencies clean check coverage race .remove_empty_dirs .pre-check-go 2 | 3 | SRCS = $(patsubst ./%,%,$(shell find . -name "*.go" -not -path "*vendor*" -not -path "*.pb.go")) 4 | PACKAGES := $(shell go list ./... | grep -v /vendor) 5 | PROTOS = $(patsubst ./%,%,$(shell find . -name "*.proto")) 6 | PBS = $(patsubst %.proto,%.pb.go,$(patsubst api%,pkg%,$(PROTOS))) 7 | MOCK_PACKAGES = \ 8 | internal/app/provider \ 9 | internal/pkg/metrics 10 | 11 | MOCKED_FILES = $(shell find . -name DOES_NOT_EXIST_FILE $(patsubst %,-or -path "./%/mocks/*.go",$(MOCK_PACKAGES))) 12 | MOCKED_FOLDERS = $(patsubst %,%/mocks,$(MOCK_PACKAGES)) 13 | 14 | help: ## Display this help screen 15 | @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 16 | 17 | generate: $(PBS) $(MOCKED_FILES) $(MOCKED_FOLDERS) cmd/postviewd/wire_gen.go | .remove_empty_dirs ## Generate all auto-generated files 18 | .remove_empty_dirs: 19 | -find . -type d -print | xargs rmdir 2>/dev/null | true 20 | 21 | dependencies: | .pre-check-go .bin/golangci-lint ## to install the dependencies 22 | go mod download 23 | 24 | clean: ## to remove generated files 25 | -rm -rf postviewd 26 | -find . -type d -name mocks -exec rm -rf \{} + 27 | 28 | postviewd: $(SRCS) $(PBS) | generate ## Compile postview daemon 29 | go build -o $@ -ldflags="$(LD_FLAGS)" ./cmd/$@ 30 | 31 | docker: ## to build docker image 32 | $(DOCKER) build -t $(IMAGE_NAME):$(IMAGE_VERSION) . 33 | 34 | push: docker ## to push docker image to registry 35 | $(DOCKER) push $(IMAGE_NAME):$(VERSION) 36 | 37 | push-production: ## to tag and push :production tag on docker image 38 | $(DOCKER) pull $(IMAGE_NAME):$(IMAGE_VERSION) 39 | $(DOCKER) tag $(IMAGE_NAME):$(IMAGE_VERSION) $(IMAGE_NAME):production 40 | $(DOCKER) push $(IMAGE_NAME):production 41 | 42 | deploy: ## to deploy it on kubernetes 43 | kubectl --namespace divar-review patch deployment/postview -p='{"spec":{"template":{"spec":{"containers":[{"name":"postview","imagePullPolicy":"IfNotPresent"}]}}}}' || echo "No Need To Patch Config" 44 | kubectl --namespace divar-review set image deployment/postview postview=$(IMAGE_NAME):$(VERSION) 45 | 46 | lint: .bin/golangci-lint ## to lint the files 47 | .bin/golangci-lint run --config=.golangci-lint.yml ./... 48 | 49 | .bin/golangci-lint: 50 | if [ -z "$$(which golangci-lint)" ]; then curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b .bin/ $(LINTER_VERSION); else mkdir -p .bin; ln -s "$$(which golangci-lint)" $@; fi 51 | 52 | fmt: ## to run `go fmt` on all source code 53 | gofmt -s -w $(SRCS) 54 | 55 | check: | generate ## Run tests 56 | go test ./... 57 | 58 | race: | generate ## to run data race detector 59 | go test -timeout 30s -race ./... 60 | 61 | coverage: coverage.cover coverage.html ## to run tests and generate test coverage data 62 | gocov convert $< | gocov report 63 | 64 | coverage.html: coverage.cover 65 | go tool cover -html=$< -o $@ 66 | 67 | coverage.cover: $(SRCS) $(PBS) Makefile | generate 68 | -rm -rfv .coverage 69 | mkdir -p .coverage 70 | $(foreach pkg,$(PACKAGES),go test -timeout 30s -short -covermode=count -coverprofile=.coverage/$(subst /,-,$(pkg)).cover $(pkg)${\n}) 71 | echo "mode: count" > $@ 72 | grep -h -v "^mode:" .coverage/*.cover >> $@ 73 | 74 | cmd/postviewd/wire_gen.go: cmd/postviewd/container.go 75 | wire ./cmd/postviewd 76 | 77 | .SECONDEXPANSION: 78 | $(PBS): $$(patsubst %.pb.go,%.proto,$$(patsubst pkg%,api%,$$@)) | .pre-check-go 79 | $(PROTOC) $(PROTOC_OPTIONS) --go_out=plugins=grpc:$(GOPATH)/src ./$< 80 | 81 | .SECONDEXPANSION: 82 | $(MOCKED_FOLDERS): | .pre-check-go 83 | cd $(patsubst %/mocks,%,$@) && mockery -all -outpkg mocks -output mocks 84 | 85 | .SECONDEXPANSION: 86 | $(MOCKED_FILES): $$(shell find $$(patsubst %/mocks,%,$$(patsubst %/mocks/,%,$$(dir $$@))) -maxdepth 1 -name "*.go") | $(MOCKED_FOLDERS) 87 | rm -rf $(dir $@) 88 | cd $(patsubst %/mocks,%,$(patsubst %/mocks/,%,$(dir $@))) && mockery -all -outpkg mocks -output mocks 89 | 90 | .pre-check-go: 91 | if [ -z "$$(which protoc-gen-go)" ]; then go get -v github.com/golang/protobuf/protoc-gen-go; fi 92 | if [ -z "$$(which mockery)" ]; then go get -v github.com/vektra/mockery/cmd/mockery; fi 93 | if [ -z "$$(which gocov)" ]; then go get -v github.com/axw/gocov/gocov; fi 94 | if [ -z "$$(which wire)" ]; then go get -v github.com/google/wire/cmd/wire; fi 95 | 96 | # Variables 97 | ROOT := github.com/cafebazaar/go-boilerplate 98 | 99 | PROTOC ?= protoc 100 | PROTOC_OPTIONS ?= -I. 101 | LINTER_VERSION = v1.12.5 102 | GIT ?= git 103 | DOCKER ?= docker 104 | COMMIT := $(shell $(GIT) rev-parse HEAD) 105 | CI_COMMIT_TAG ?= 106 | VERSION ?= $(strip $(if $(CI_COMMIT_TAG),$(CI_COMMIT_TAG),$(shell $(GIT) describe --tag 2> /dev/null || echo "$(COMMIT)"))) 107 | BUILD_TIME := $(shell LANG=en_US date +"%F_%T_%z") 108 | LD_FLAGS := -X $(ROOT)/pkg/postview.Version=$(VERSION) -X $(ROOT)/pkg/postview.Commit=$(COMMIT) -X $(ROOT)/pkg/postview.BuildTime=$(BUILD_TIME) 109 | IMAGE_NAME ?= registry.cafebazaar.ir:5000/arcana261/golang-boilerplate 110 | IMAGE_VERSION ?= $(VERSION) 111 | 112 | # Helper Variables 113 | 114 | # a variable containing a new line e.g. 115 | # ${\n} would emit a new line 116 | # useful in $(foreach functions 117 | define \n 118 | 119 | 120 | endef 121 | -------------------------------------------------------------------------------- /pkg/postview/postview.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: api/postview/postview.proto 3 | 4 | package postview 5 | 6 | import ( 7 | context "context" 8 | fmt "fmt" 9 | proto "github.com/golang/protobuf/proto" 10 | timestamp "github.com/golang/protobuf/ptypes/timestamp" 11 | grpc "google.golang.org/grpc" 12 | math "math" 13 | ) 14 | 15 | // Reference imports to suppress errors if they are not otherwise used. 16 | var _ = proto.Marshal 17 | var _ = fmt.Errorf 18 | var _ = math.Inf 19 | 20 | // This is a compile-time assertion to ensure that this generated file 21 | // is compatible with the proto package it is being compiled against. 22 | // A compilation error at this line likely means your copy of the 23 | // proto package needs to be updated. 24 | const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package 25 | 26 | type GetPostRequest struct { 27 | Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` 28 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 29 | XXX_unrecognized []byte `json:"-"` 30 | XXX_sizecache int32 `json:"-"` 31 | } 32 | 33 | func (m *GetPostRequest) Reset() { *m = GetPostRequest{} } 34 | func (m *GetPostRequest) String() string { return proto.CompactTextString(m) } 35 | func (*GetPostRequest) ProtoMessage() {} 36 | func (*GetPostRequest) Descriptor() ([]byte, []int) { 37 | return fileDescriptor_d493abf9d07462f8, []int{0} 38 | } 39 | 40 | func (m *GetPostRequest) XXX_Unmarshal(b []byte) error { 41 | return xxx_messageInfo_GetPostRequest.Unmarshal(m, b) 42 | } 43 | func (m *GetPostRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 44 | return xxx_messageInfo_GetPostRequest.Marshal(b, m, deterministic) 45 | } 46 | func (m *GetPostRequest) XXX_Merge(src proto.Message) { 47 | xxx_messageInfo_GetPostRequest.Merge(m, src) 48 | } 49 | func (m *GetPostRequest) XXX_Size() int { 50 | return xxx_messageInfo_GetPostRequest.Size(m) 51 | } 52 | func (m *GetPostRequest) XXX_DiscardUnknown() { 53 | xxx_messageInfo_GetPostRequest.DiscardUnknown(m) 54 | } 55 | 56 | var xxx_messageInfo_GetPostRequest proto.InternalMessageInfo 57 | 58 | func (m *GetPostRequest) GetToken() string { 59 | if m != nil { 60 | return m.Token 61 | } 62 | return "" 63 | } 64 | 65 | type GetPostResponse struct { 66 | Post *Post `protobuf:"bytes,1,opt,name=post,proto3" json:"post,omitempty"` 67 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 68 | XXX_unrecognized []byte `json:"-"` 69 | XXX_sizecache int32 `json:"-"` 70 | } 71 | 72 | func (m *GetPostResponse) Reset() { *m = GetPostResponse{} } 73 | func (m *GetPostResponse) String() string { return proto.CompactTextString(m) } 74 | func (*GetPostResponse) ProtoMessage() {} 75 | func (*GetPostResponse) Descriptor() ([]byte, []int) { 76 | return fileDescriptor_d493abf9d07462f8, []int{1} 77 | } 78 | 79 | func (m *GetPostResponse) XXX_Unmarshal(b []byte) error { 80 | return xxx_messageInfo_GetPostResponse.Unmarshal(m, b) 81 | } 82 | func (m *GetPostResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 83 | return xxx_messageInfo_GetPostResponse.Marshal(b, m, deterministic) 84 | } 85 | func (m *GetPostResponse) XXX_Merge(src proto.Message) { 86 | xxx_messageInfo_GetPostResponse.Merge(m, src) 87 | } 88 | func (m *GetPostResponse) XXX_Size() int { 89 | return xxx_messageInfo_GetPostResponse.Size(m) 90 | } 91 | func (m *GetPostResponse) XXX_DiscardUnknown() { 92 | xxx_messageInfo_GetPostResponse.DiscardUnknown(m) 93 | } 94 | 95 | var xxx_messageInfo_GetPostResponse proto.InternalMessageInfo 96 | 97 | func (m *GetPostResponse) GetPost() *Post { 98 | if m != nil { 99 | return m.Post 100 | } 101 | return nil 102 | } 103 | 104 | type Post struct { 105 | Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` 106 | Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` 107 | Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` 108 | Thumbnail string `protobuf:"bytes,4,opt,name=thumbnail,proto3" json:"thumbnail,omitempty"` 109 | Contact *Contact `protobuf:"bytes,5,opt,name=contact,proto3" json:"contact,omitempty"` 110 | PublishedAt *timestamp.Timestamp `protobuf:"bytes,6,opt,name=published_at,json=publishedAt,proto3" json:"published_at,omitempty"` 111 | CategorySlugs []string `protobuf:"bytes,7,rep,name=category_slugs,json=categorySlugs,proto3" json:"category_slugs,omitempty"` 112 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 113 | XXX_unrecognized []byte `json:"-"` 114 | XXX_sizecache int32 `json:"-"` 115 | } 116 | 117 | func (m *Post) Reset() { *m = Post{} } 118 | func (m *Post) String() string { return proto.CompactTextString(m) } 119 | func (*Post) ProtoMessage() {} 120 | func (*Post) Descriptor() ([]byte, []int) { 121 | return fileDescriptor_d493abf9d07462f8, []int{2} 122 | } 123 | 124 | func (m *Post) XXX_Unmarshal(b []byte) error { 125 | return xxx_messageInfo_Post.Unmarshal(m, b) 126 | } 127 | func (m *Post) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 128 | return xxx_messageInfo_Post.Marshal(b, m, deterministic) 129 | } 130 | func (m *Post) XXX_Merge(src proto.Message) { 131 | xxx_messageInfo_Post.Merge(m, src) 132 | } 133 | func (m *Post) XXX_Size() int { 134 | return xxx_messageInfo_Post.Size(m) 135 | } 136 | func (m *Post) XXX_DiscardUnknown() { 137 | xxx_messageInfo_Post.DiscardUnknown(m) 138 | } 139 | 140 | var xxx_messageInfo_Post proto.InternalMessageInfo 141 | 142 | func (m *Post) GetTitle() string { 143 | if m != nil { 144 | return m.Title 145 | } 146 | return "" 147 | } 148 | 149 | func (m *Post) GetDescription() string { 150 | if m != nil { 151 | return m.Description 152 | } 153 | return "" 154 | } 155 | 156 | func (m *Post) GetToken() string { 157 | if m != nil { 158 | return m.Token 159 | } 160 | return "" 161 | } 162 | 163 | func (m *Post) GetThumbnail() string { 164 | if m != nil { 165 | return m.Thumbnail 166 | } 167 | return "" 168 | } 169 | 170 | func (m *Post) GetContact() *Contact { 171 | if m != nil { 172 | return m.Contact 173 | } 174 | return nil 175 | } 176 | 177 | func (m *Post) GetPublishedAt() *timestamp.Timestamp { 178 | if m != nil { 179 | return m.PublishedAt 180 | } 181 | return nil 182 | } 183 | 184 | func (m *Post) GetCategorySlugs() []string { 185 | if m != nil { 186 | return m.CategorySlugs 187 | } 188 | return nil 189 | } 190 | 191 | type Contact struct { 192 | Phone string `protobuf:"bytes,1,opt,name=phone,proto3" json:"phone,omitempty"` 193 | Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` 194 | Chat bool `protobuf:"varint,4,opt,name=chat,proto3" json:"chat,omitempty"` 195 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 196 | XXX_unrecognized []byte `json:"-"` 197 | XXX_sizecache int32 `json:"-"` 198 | } 199 | 200 | func (m *Contact) Reset() { *m = Contact{} } 201 | func (m *Contact) String() string { return proto.CompactTextString(m) } 202 | func (*Contact) ProtoMessage() {} 203 | func (*Contact) Descriptor() ([]byte, []int) { 204 | return fileDescriptor_d493abf9d07462f8, []int{3} 205 | } 206 | 207 | func (m *Contact) XXX_Unmarshal(b []byte) error { 208 | return xxx_messageInfo_Contact.Unmarshal(m, b) 209 | } 210 | func (m *Contact) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 211 | return xxx_messageInfo_Contact.Marshal(b, m, deterministic) 212 | } 213 | func (m *Contact) XXX_Merge(src proto.Message) { 214 | xxx_messageInfo_Contact.Merge(m, src) 215 | } 216 | func (m *Contact) XXX_Size() int { 217 | return xxx_messageInfo_Contact.Size(m) 218 | } 219 | func (m *Contact) XXX_DiscardUnknown() { 220 | xxx_messageInfo_Contact.DiscardUnknown(m) 221 | } 222 | 223 | var xxx_messageInfo_Contact proto.InternalMessageInfo 224 | 225 | func (m *Contact) GetPhone() string { 226 | if m != nil { 227 | return m.Phone 228 | } 229 | return "" 230 | } 231 | 232 | func (m *Contact) GetEmail() string { 233 | if m != nil { 234 | return m.Email 235 | } 236 | return "" 237 | } 238 | 239 | func (m *Contact) GetChat() bool { 240 | if m != nil { 241 | return m.Chat 242 | } 243 | return false 244 | } 245 | 246 | func init() { 247 | proto.RegisterType((*GetPostRequest)(nil), "postview.GetPostRequest") 248 | proto.RegisterType((*GetPostResponse)(nil), "postview.GetPostResponse") 249 | proto.RegisterType((*Post)(nil), "postview.Post") 250 | proto.RegisterType((*Contact)(nil), "postview.Contact") 251 | } 252 | 253 | func init() { proto.RegisterFile("api/postview/postview.proto", fileDescriptor_d493abf9d07462f8) } 254 | 255 | var fileDescriptor_d493abf9d07462f8 = []byte{ 256 | // 394 bytes of a gzipped FileDescriptorProto 257 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x52, 0xcf, 0x8b, 0xd4, 0x30, 258 | 0x14, 0x66, 0x77, 0xbb, 0xdb, 0x99, 0x8c, 0x8e, 0x18, 0x3c, 0xd4, 0x51, 0x70, 0x28, 0x28, 0x0b, 259 | 0x62, 0x8b, 0x23, 0x7a, 0x91, 0x15, 0xd4, 0x83, 0xe8, 0x49, 0xaa, 0x78, 0xf0, 0xb2, 0xbc, 0x76, 260 | 0xdf, 0xa4, 0x61, 0xd2, 0x24, 0x36, 0xaf, 0x0e, 0x7a, 0xf7, 0xff, 0x96, 0xa4, 0xd3, 0x76, 0x04, 261 | 0x6f, 0xf9, 0x7e, 0x34, 0xf9, 0xde, 0xf7, 0xca, 0x1e, 0x80, 0x95, 0xb9, 0x35, 0x8e, 0x7e, 0x4a, 262 | 0xdc, 0x8f, 0x87, 0xcc, 0xb6, 0x86, 0x0c, 0x9f, 0x0d, 0x78, 0xf5, 0x48, 0x18, 0x23, 0x14, 0xe6, 263 | 0x81, 0x2f, 0xbb, 0x6d, 0x4e, 0xb2, 0x41, 0x47, 0xd0, 0xd8, 0xde, 0x9a, 0x3e, 0x61, 0xcb, 0x0f, 264 | 0x48, 0x9f, 0x8d, 0xa3, 0x02, 0x7f, 0x74, 0xe8, 0x88, 0xdf, 0x63, 0xe7, 0x64, 0x76, 0xa8, 0x93, 265 | 0x93, 0xf5, 0xc9, 0xe5, 0xbc, 0xe8, 0x41, 0xfa, 0x92, 0xdd, 0x19, 0x7d, 0xce, 0x1a, 0xed, 0x90, 266 | 0xa7, 0x2c, 0xf2, 0xef, 0x04, 0xdf, 0x62, 0xb3, 0xcc, 0xc6, 0x10, 0xc1, 0x15, 0xb4, 0xf4, 0xcf, 267 | 0x29, 0x8b, 0x3c, 0x0c, 0xb7, 0x4a, 0x52, 0x38, 0xde, 0xea, 0x01, 0x5f, 0xb3, 0xc5, 0x0d, 0xba, 268 | 0xaa, 0x95, 0x96, 0xa4, 0xd1, 0xc9, 0x69, 0xd0, 0x8e, 0xa9, 0x29, 0xcd, 0xd9, 0x51, 0x1a, 0xfe, 269 | 0x90, 0xcd, 0xa9, 0xee, 0x9a, 0x52, 0x83, 0x54, 0x49, 0x14, 0x94, 0x89, 0xe0, 0x4f, 0x59, 0x5c, 270 | 0x19, 0x4d, 0x50, 0x51, 0x72, 0x1e, 0xb2, 0xdd, 0x9d, 0xb2, 0xbd, 0xef, 0x85, 0x62, 0x70, 0xf0, 271 | 0x2b, 0x76, 0xcb, 0x76, 0xa5, 0x92, 0xae, 0xc6, 0x9b, 0x6b, 0xa0, 0xe4, 0x22, 0x7c, 0xb1, 0xca, 272 | 0xfa, 0xe2, 0xb2, 0xa1, 0xb8, 0xec, 0xeb, 0x50, 0x5c, 0xb1, 0x18, 0xfd, 0x6f, 0x89, 0x3f, 0x66, 273 | 0xcb, 0x0a, 0x08, 0x85, 0x69, 0x7f, 0x5d, 0x3b, 0xd5, 0x09, 0x97, 0xc4, 0xeb, 0xb3, 0xcb, 0x79, 274 | 0x71, 0x7b, 0x60, 0xbf, 0x78, 0x32, 0xfd, 0xc8, 0xe2, 0xc3, 0xcb, 0x7e, 0x22, 0x5b, 0x1b, 0x3d, 275 | 0x36, 0x11, 0x80, 0x67, 0xb1, 0xf1, 0xd3, 0xf4, 0x1d, 0xf4, 0x80, 0x73, 0x16, 0x55, 0x35, 0x50, 276 | 0x18, 0x71, 0x56, 0x84, 0xf3, 0xe6, 0x13, 0x9b, 0xf9, 0x46, 0xbf, 0x49, 0xdc, 0xf3, 0x37, 0x2c, 277 | 0x3e, 0x6c, 0x85, 0x27, 0xd3, 0x8c, 0xff, 0x2e, 0x74, 0x75, 0xff, 0x3f, 0x4a, 0xbf, 0xc2, 0x77, 278 | 0x57, 0xdf, 0x5f, 0x0b, 0x49, 0x59, 0x05, 0x5b, 0x2c, 0xe1, 0x37, 0x40, 0x9b, 0xc9, 0x36, 0x87, 279 | 0xb6, 0x02, 0x0d, 0x9b, 0x57, 0xcf, 0x73, 0x61, 0x14, 0x68, 0xf1, 0xac, 0x34, 0x52, 0x61, 0x6b, 280 | 0x15, 0x10, 0xe6, 0x76, 0x27, 0xc6, 0xbf, 0xad, 0xbc, 0x08, 0xed, 0xbc, 0xf8, 0x1b, 0x00, 0x00, 281 | 0xff, 0xff, 0x41, 0x82, 0xbe, 0x4a, 0x8d, 0x02, 0x00, 0x00, 282 | } 283 | 284 | // Reference imports to suppress errors if they are not otherwise used. 285 | var _ context.Context 286 | var _ grpc.ClientConn 287 | 288 | // This is a compile-time assertion to ensure that this generated file 289 | // is compatible with the grpc package it is being compiled against. 290 | const _ = grpc.SupportPackageIsVersion4 291 | 292 | // PostViewClient is the client API for PostView service. 293 | // 294 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 295 | type PostViewClient interface { 296 | GetPost(ctx context.Context, in *GetPostRequest, opts ...grpc.CallOption) (*GetPostResponse, error) 297 | } 298 | 299 | type postViewClient struct { 300 | cc *grpc.ClientConn 301 | } 302 | 303 | func NewPostViewClient(cc *grpc.ClientConn) PostViewClient { 304 | return &postViewClient{cc} 305 | } 306 | 307 | func (c *postViewClient) GetPost(ctx context.Context, in *GetPostRequest, opts ...grpc.CallOption) (*GetPostResponse, error) { 308 | out := new(GetPostResponse) 309 | err := c.cc.Invoke(ctx, "/postview.PostView/GetPost", in, out, opts...) 310 | if err != nil { 311 | return nil, err 312 | } 313 | return out, nil 314 | } 315 | 316 | // PostViewServer is the server API for PostView service. 317 | type PostViewServer interface { 318 | GetPost(context.Context, *GetPostRequest) (*GetPostResponse, error) 319 | } 320 | 321 | func RegisterPostViewServer(s *grpc.Server, srv PostViewServer) { 322 | s.RegisterService(&_PostView_serviceDesc, srv) 323 | } 324 | 325 | func _PostView_GetPost_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 326 | in := new(GetPostRequest) 327 | if err := dec(in); err != nil { 328 | return nil, err 329 | } 330 | if interceptor == nil { 331 | return srv.(PostViewServer).GetPost(ctx, in) 332 | } 333 | info := &grpc.UnaryServerInfo{ 334 | Server: srv, 335 | FullMethod: "/postview.PostView/GetPost", 336 | } 337 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 338 | return srv.(PostViewServer).GetPost(ctx, req.(*GetPostRequest)) 339 | } 340 | return interceptor(ctx, in, info, handler) 341 | } 342 | 343 | var _PostView_serviceDesc = grpc.ServiceDesc{ 344 | ServiceName: "postview.PostView", 345 | HandlerType: (*PostViewServer)(nil), 346 | Methods: []grpc.MethodDesc{ 347 | { 348 | MethodName: "GetPost", 349 | Handler: _PostView_GetPost_Handler, 350 | }, 351 | }, 352 | Streams: []grpc.StreamDesc{}, 353 | Metadata: "api/postview/postview.proto", 354 | } 355 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.33.1/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU= 5 | cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= 6 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 7 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 8 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 9 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= 10 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 11 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 12 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 13 | github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc= 14 | github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= 15 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 16 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 17 | github.com/axw/gocov v1.0.0 h1:YsqYR66hUmilVr23tu8USgnJIJvnwh3n7j5zRn7x4LU= 18 | github.com/axw/gocov v1.0.0/go.mod h1:LvQpEYiwwIb2nYkXY2fDWhg9/AsYqkhmrCshjlUJECE= 19 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 20 | github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= 21 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 22 | github.com/certifi/gocertifi v0.0.0-20190506164543-d2eda7129713 h1:UNOqI3EKhvbqV8f1Vm3NIwkrhq388sGCeAH2Op7w0rc= 23 | github.com/certifi/gocertifi v0.0.0-20190506164543-d2eda7129713/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= 24 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 25 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 26 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 27 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 28 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 29 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 30 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 31 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 32 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 33 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 34 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 35 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 36 | github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc= 37 | github.com/denisenkom/go-mssqldb v0.0.0-20190423183735-731ef375ac02 h1:PS3xfVPa8N84AzoWZHFCbA0+ikz4f4skktfjQoNMsgk= 38 | github.com/denisenkom/go-mssqldb v0.0.0-20190423183735-731ef375ac02/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= 39 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 40 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 41 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 42 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 43 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 44 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 45 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 46 | github.com/evalphobia/logrus_sentry v0.8.2 h1:dotxHq+YLZsT1Bb45bB5UQbfCh3gM/nFFetyN46VoDQ= 47 | github.com/evalphobia/logrus_sentry v0.8.2/go.mod h1:pKcp+vriitUqu9KiWj/VRFbRfFNUwz95/UkgG8a6MNc= 48 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 49 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 50 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 51 | github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= 52 | github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= 53 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 54 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 55 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 56 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 57 | github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4= 58 | github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 59 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 60 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 61 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 62 | github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 63 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 64 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 65 | github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= 66 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 67 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 68 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 69 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 70 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 71 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 72 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 73 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 74 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 75 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 76 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 77 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 78 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 79 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 80 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 81 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 82 | github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k= 83 | github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 84 | github.com/google/wire v0.3.0 h1:imGQZGEVEHpje5056+K+cgdO72p0LQv2xIIFXNGUf60= 85 | github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s= 86 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 87 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 88 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 89 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 90 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= 91 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 92 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= 93 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 94 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 95 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 96 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 97 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 98 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 99 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 100 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 101 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 102 | github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= 103 | github.com/jinzhu/gorm v1.9.8 h1:n5uvxqLepIP2R1XF7pudpt9Rv8I3m7G9trGxJVjLZ5k= 104 | github.com/jinzhu/gorm v1.9.8/go.mod h1:bdqTT3q6dhSph2K3pWxrHP6nqxuAp2yQ3KFtc3U3F84= 105 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k= 106 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 107 | github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc= 108 | github.com/jinzhu/now v1.0.0 h1:6WV8LvwPpDhKjo5U9O6b4+xdG/jTXNPwlDme/MTo8Ns= 109 | github.com/jinzhu/now v1.0.0/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc= 110 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 111 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 112 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 113 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 114 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 115 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 116 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 117 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 118 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 119 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 120 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 121 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 122 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 123 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 124 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 125 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 126 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 127 | github.com/lib/pq v1.1.0 h1:/5u4a+KGJptBRqGzPvYQL9p0d/tPR4S31+Tnzj9lEO4= 128 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 129 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= 130 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 131 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 132 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 133 | github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= 134 | github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 135 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 136 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 137 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 138 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 139 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 140 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 141 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 142 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 143 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 144 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 145 | github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= 146 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 147 | github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= 148 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 149 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= 150 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 151 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 152 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 153 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 154 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 155 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 156 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 157 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 158 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= 159 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 160 | github.com/prometheus/client_golang v0.9.4 h1:Y8E/JaaPbmFSW2V81Ab/d8yZFYQQGbni1b1jPcG9Y6A= 161 | github.com/prometheus/client_golang v0.9.4/go.mod h1:oCXIBxdI62A4cR6aTRJCgetEjecSIYzOEaeAn4iYEpM= 162 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 163 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 164 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= 165 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 166 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 167 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 168 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 169 | github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= 170 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 171 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 172 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 173 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 174 | github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= 175 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 176 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 177 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 178 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 179 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 180 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 181 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 182 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 183 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 184 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 185 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 186 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 187 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 188 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 189 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 190 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 191 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 192 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 193 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 194 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 195 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 196 | github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= 197 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 198 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 199 | github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 200 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 201 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 202 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 203 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 204 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 205 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 206 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 207 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 208 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 209 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 210 | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 211 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 212 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 213 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 214 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 215 | golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 216 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 217 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 218 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= 219 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 220 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 221 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 222 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 223 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 224 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 225 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 226 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 227 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 228 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 229 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 230 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 231 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 232 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 233 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 234 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= 235 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 236 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 237 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 238 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 239 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 240 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 241 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 242 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 243 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 244 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 245 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 246 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 247 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 248 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 249 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 250 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 251 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 252 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 253 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 254 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 255 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= 256 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 257 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 258 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 259 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 260 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 261 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 262 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 263 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 264 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138 h1:H3uGjxCR/6Ds0Mjgyp7LMK81+LvmbvWWEnJhzk1Pi9E= 265 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 266 | golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b h1:NVD8gBK33xpdqCaZVVtd6OFJp+3dxkXuz7+U7KaVN6s= 267 | golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 268 | golang.org/x/tools v0.0.0-20190617190820-da514acc4774 h1:CQVOmarCBFzTx0kbOU0ru54Cvot8SdSrNYjZPhQl+gk= 269 | golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 270 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4= 271 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 272 | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= 273 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 274 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 275 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 276 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 277 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 278 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 279 | google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107 h1:xtNn7qFlagY2mQNFHMSRPjT2RkOV4OXM7P5TVy9xATo= 280 | google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 281 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 282 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 283 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 284 | google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= 285 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 286 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 287 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 288 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 289 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 290 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 291 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 292 | gopkg.in/gormigrate.v1 v1.5.0 h1:M667uzFRcnBf5cNAcSyYyNdJTJ1KQnUTmc+mjCLfPfw= 293 | gopkg.in/gormigrate.v1 v1.5.0/go.mod h1:Lf00lQrHqfSYWiTtPcyQabsDdM6ejZaMgV0OU6JMSlw= 294 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 295 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 296 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 297 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 298 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 299 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 300 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 301 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 302 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 303 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 304 | --------------------------------------------------------------------------------