├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── cmd └── main.go ├── config ├── config.go ├── config.yaml ├── mappings.json ├── search_query.json └── translate.json ├── docker-compose.local.yaml ├── docker-compose.yaml ├── go.mod ├── go.sum ├── internal ├── app │ ├── app.go │ ├── elastic.go │ ├── healthcheck.go │ ├── http_server.go │ ├── keymappings.go │ ├── metrics.go │ ├── rabbitmq.go │ └── utils.go ├── dto │ └── search_producrs_response.go ├── metrics │ └── metrics.go └── product │ ├── delivery │ ├── http │ │ └── v1 │ │ │ ├── controller.go │ │ │ ├── controller_test.go │ │ │ └── routes.go │ └── rabbitmq │ │ ├── consumer.go │ │ └── consumer_test.go │ ├── domain │ ├── model.go │ ├── repository.go │ ├── search.go │ └── usecase.go │ ├── repository │ └── elastic_repository.go │ └── usecase │ └── usecase.go ├── k8s └── microservice │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── elastic.yaml │ ├── jaeger.yaml │ ├── kibana.yaml │ ├── rabbit.yaml │ └── search-microservice.yaml │ └── values.yaml ├── monitoring └── prometheus.yml └── pkg ├── constants └── constants.go ├── elastic └── elastic.go ├── esclient ├── esclient.go └── search_query.go ├── http_client └── http_client.go ├── http_errors └── http_errors.go ├── keyboard_manager └── keyboard_manager.go ├── logger └── logger.go ├── middlewares └── manager.go ├── probes └── probes.go ├── rabbitmq ├── publisher.go └── rabbitmq.go ├── serializer └── serializer.go ├── tracing ├── jaeger.go └── utils.go └── utils └── pagination.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | kafka_data 3 | zookeeper 4 | pgdata 5 | prometheus 6 | mongodb_data_container 7 | grafana 8 | microservices_pgdata 9 | es-data01 10 | main 11 | volume -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19-alpine AS builder 2 | 3 | # Set necessary environmet variables needed for our image 4 | ENV CGO_ENABLED=0 5 | 6 | # Move to working directory /build 7 | WORKDIR /build 8 | 9 | # Copy and download dependency using go mod 10 | COPY go.mod . 11 | COPY go.sum . 12 | RUN go mod download 13 | 14 | # Copy the code into the container 15 | COPY . . 16 | 17 | # Build the application 18 | # go build -o [name] [path to file] 19 | RUN go build -o app cmd/main.go 20 | 21 | # Move to /dist directory as the place for resulting binary folder 22 | WORKDIR /dist 23 | 24 | # Copy binary from build to main folder 25 | RUN cp /build/app . 26 | 27 | ############################ 28 | # STEP 2 build a small image 29 | ############################ 30 | FROM alpine:latest 31 | RUN apk --no-cache add ca-certificates 32 | 33 | COPY . . 34 | COPY --from=builder /dist/app / 35 | # Copy the code into the container 36 | 37 | # Command to run the executable 38 | ENTRYPOINT ["/app"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: 2 | 3 | # ============================================================================== 4 | # Docker 5 | 6 | local: 7 | @echo Clearing elasticserach data 8 | rm -rf ./es-data01 9 | @echo Clearing prometheus data 10 | rm -rf ./prometheus 11 | @echo Starting local docker compose 12 | docker-compose -f docker-compose.local.yaml up -d --build 13 | 14 | develop: 15 | @echo Clearing elasticserach data 16 | rm -rf ./es-data01 17 | @echo Clearing prometheus data 18 | rm -rf ./prometheus 19 | @echo Starting local docker compose 20 | docker-compose -f docker-compose.yaml up -d --build 21 | 22 | upload: 23 | docker build -t alexanderbryksin/search_microservice:latest -f ./Dockerfile . 24 | docker push alexanderbryksin/search_microservice:latest 25 | 26 | # ============================================================================== 27 | # Docker support 28 | 29 | FILES := $(shell docker ps -aq) 30 | 31 | down-local: 32 | docker stop $(FILES) 33 | docker rm $(FILES) 34 | 35 | clean: 36 | docker system prune -f 37 | 38 | logs-local: 39 | docker logs -f $(FILES) 40 | 41 | 42 | # ============================================================================== 43 | # k8s support 44 | 45 | install_all: 46 | @echo Updating helm repositories 47 | helm repo update 48 | @echo Creating monitoring namespace 49 | kubectl create namespace monitoring 50 | @echo Creating monitoring helm chart 51 | helm install monitoring prometheus-community/kube-prometheus-stack -n monitoring 52 | @echo Creating search microservice helm chart 53 | helm install -f k8s/microservice/values.yaml search k8s/microservice 54 | 55 | helm_install: 56 | kubens default 57 | helm install -f k8s/microservice/values.yaml search k8s/microservice 58 | 59 | uninstall_all: 60 | kubens monitoring 61 | helm uninstall monitoring 62 | kubens default 63 | helm uninstall search 64 | 65 | dry_run: 66 | kubens default 67 | kubectl apply --dry-run=client -f k8s/microservice/templates/servicemonitor.yaml 68 | 69 | port_forward_search_microservice: 70 | kubens default 71 | kubectl port-forward services/microservice-service 8000:8000 72 | 73 | port_forward_kibana: 74 | kubens default 75 | kubectl port-forward services/kibana 5601:5601 76 | 77 | port_forward_rabbitmq: 78 | kubens default 79 | kubectl port-forward services/rabbitmq 15672:15672 80 | 81 | port_forward_prometheus: 82 | kubens monitoring 83 | kubectl port-forward services/monitoring-kube-prometheus-prometheus 9090:9090 84 | 85 | minikube_start: 86 | minikube start --memory 16000 --cpus 4 87 | 88 | monitoring_install: 89 | helm repo update 90 | kubectl create namespace monitoring 91 | helm install monitoring prometheus-community/kube-prometheus-stack -n monitoring 92 | 93 | monitoring_uninstall: 94 | helm uninstall prometheus 95 | 96 | port_forward_search_service_monitor: 97 | kubens monitoring 98 | kubectl port-forward services/monitoring-kube-prometheus-prometheus 9090:9090 99 | 100 | apply_search_service_monitor: 101 | kubectl apply -f k8s/microservice/servicemonitor.yaml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Go ElasticSearch and RabbitMQ full-text search microservice 👋✨💫 2 | 3 | #### 👨‍💻 Full list what has been used: 4 | [Elasticsearch](https://github.com/elastic/go-elasticsearch) client for Go
5 | [RabbitMQ](https://github.com/rabbitmq/amqp091-go) Go RabbitMQ Client Library
6 | [Jaeger](https://www.jaegertracing.io/) open source, end-to-end distributed [tracing](https://opentracing.io/)
7 | [Prometheus](https://prometheus.io/) monitoring and alerting
8 | [Grafana](https://grafana.com/) for to compose observability dashboards with everything from Prometheus
9 | [Echo](https://github.com/labstack/echo) web framework
10 | [Kibana](https://github.com/labstack/echo) is user interface that lets you visualize your Elasticsearch
11 | [Docker](https://www.docker.com/) and docker-compose
12 | [Kubernetes](https://kubernetes.io/) K8s
13 | [Helm](https://helm.sh/) The package manager for Kubernetes
14 | 15 | 16 | ### RabbitMQ UI: 17 | 18 | http://localhost:15672 19 | 20 | ### Jaeger UI: 21 | 22 | http://localhost:16686 23 | 24 | ### Prometheus UI: 25 | 26 | http://localhost:9090 27 | 28 | ### Grafana UI: 29 | 30 | http://localhost:3005 31 | 32 | ### Kibana UI: 33 | 34 | http://localhost:5601/app/home#/ 35 | 36 | 37 | For local development 🙌👨‍💻🚀: 38 | 39 | ``` 40 | make local // for run docker compose 41 | make run_es // run microservice 42 | ``` 43 | or 44 | ``` 45 | make develop // run all in docker compose 46 | ``` 47 | 48 | for k8s 49 | ``` 50 | make install_all // helm install 51 | ``` -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/AleksK1NG/go-elasticsearch/config" 6 | "github.com/AleksK1NG/go-elasticsearch/internal/app" 7 | "github.com/AleksK1NG/go-elasticsearch/pkg/logger" 8 | "log" 9 | ) 10 | 11 | func main() { 12 | log.Println("Starting microservice") 13 | 14 | flag.Parse() 15 | 16 | cfg, err := config.InitConfig() 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | 21 | appLogger := logger.NewAppLogger(cfg.Logger) 22 | appLogger.InitLogger() 23 | appLogger.Named(app.GetMicroserviceName(cfg)) 24 | appLogger.Infof("CFG: %+v", cfg) 25 | appLogger.Fatal(app.NewApp(appLogger, cfg).Run()) 26 | } 27 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/AleksK1NG/go-elasticsearch/pkg/constants" 7 | "github.com/AleksK1NG/go-elasticsearch/pkg/elastic" 8 | "github.com/AleksK1NG/go-elasticsearch/pkg/esclient" 9 | "github.com/AleksK1NG/go-elasticsearch/pkg/logger" 10 | "github.com/AleksK1NG/go-elasticsearch/pkg/probes" 11 | "github.com/AleksK1NG/go-elasticsearch/pkg/rabbitmq" 12 | "github.com/AleksK1NG/go-elasticsearch/pkg/tracing" 13 | "github.com/pkg/errors" 14 | "os" 15 | 16 | "github.com/spf13/viper" 17 | ) 18 | 19 | var configPath string 20 | 21 | func init() { 22 | flag.StringVar(&configPath, "config", "", "Search microservice config path") 23 | } 24 | 25 | type Config struct { 26 | ServiceName string `mapstructure:"serviceName"` 27 | Logger logger.LogConfig `mapstructure:"logger"` 28 | GRPC GRPC `mapstructure:"grpc"` 29 | Timeouts Timeouts `mapstructure:"timeouts" validate:"required"` 30 | Jaeger *tracing.Config `mapstructure:"jaeger"` 31 | ElasticIndexes ElasticIndexes `mapstructure:"elasticIndexes" validate:"required"` 32 | Http Http `mapstructure:"http"` 33 | Probes probes.Config `mapstructure:"probes"` 34 | ElasticSearch elastic.Config `mapstructure:"elasticSearch" validate:"required"` 35 | RabbitMQ rabbitmq.Config `mapstructure:"rabbitmq" validate:"required"` 36 | ExchangeAndQueueBindings ExchangeAndQueueBindings `mapstructure:"exchangeAndQueueBindings" validate:"required"` 37 | BulkIndexerConfig elastic.BulkIndexerConfig `mapstructure:"bulkIndexer" validate:"required"` 38 | } 39 | 40 | type ExchangeAndQueueBindings struct { 41 | IndexProductBinding rabbitmq.ExchangeAndQueueBinding `mapstructure:"indexProductBinding" validate:"required"` 42 | } 43 | 44 | type GRPC struct { 45 | Port string `mapstructure:"port"` 46 | Development bool `mapstructure:"development"` 47 | } 48 | 49 | type Timeouts struct { 50 | PostgresInitMilliseconds int `mapstructure:"postgresInitMilliseconds" validate:"required"` 51 | PostgresInitRetryCount uint `mapstructure:"postgresInitRetryCount" validate:"required"` 52 | } 53 | 54 | type Projections struct { 55 | MongoGroup string `mapstructure:"mongoGroup" validate:"required"` 56 | MongoSubscriptionPoolSize int `mapstructure:"mongoSubscriptionPoolSize" validate:"required,gte=0"` 57 | ElasticGroup string `mapstructure:"elasticGroup" validate:"required"` 58 | ElasticSubscriptionPoolSize int `mapstructure:"elasticSubscriptionPoolSize" validate:"required,gte=0"` 59 | } 60 | 61 | type Http struct { 62 | Port string `mapstructure:"port" validate:"required"` 63 | Development bool `mapstructure:"development"` 64 | BasePath string `mapstructure:"basePath" validate:"required"` 65 | ProductsPath string `mapstructure:"productsPath" validate:"required"` 66 | DebugErrorsResponse bool `mapstructure:"debugErrorsResponse"` 67 | IgnoreLogUrls []string `mapstructure:"ignoreLogUrls"` 68 | } 69 | 70 | type ElasticIndexes struct { 71 | ProductsIndex esclient.ElasticIndex `mapstructure:"products" validate:"required"` 72 | } 73 | 74 | func InitConfig() (*Config, error) { 75 | if configPath == "" { 76 | configPathFromEnv := os.Getenv(constants.ConfigPath) 77 | if configPathFromEnv != "" { 78 | configPath = configPathFromEnv 79 | } else { 80 | getwd, err := os.Getwd() 81 | if err != nil { 82 | return nil, errors.Wrap(err, "os.Getwd") 83 | } 84 | configPath = fmt.Sprintf("%s/config/config.yaml", getwd) 85 | } 86 | } 87 | 88 | cfg := &Config{} 89 | 90 | viper.SetConfigType(constants.Yaml) 91 | viper.SetConfigFile(configPath) 92 | 93 | if err := viper.ReadInConfig(); err != nil { 94 | return nil, errors.Wrap(err, "viper.ReadInConfig") 95 | } 96 | 97 | if err := viper.Unmarshal(cfg); err != nil { 98 | return nil, errors.Wrap(err, "viper.Unmarshal") 99 | } 100 | 101 | grpcPort := os.Getenv(constants.GrpcPort) 102 | if grpcPort != "" { 103 | cfg.GRPC.Port = grpcPort 104 | } 105 | 106 | jaegerAddr := os.Getenv(constants.JaegerHostPort) 107 | if jaegerAddr != "" { 108 | cfg.Jaeger.HostPort = jaegerAddr 109 | } 110 | 111 | elasticUrl := os.Getenv(constants.ElasticUrl) 112 | if elasticUrl != "" { 113 | cfg.ElasticSearch.Addresses = []string{elasticUrl} 114 | } 115 | 116 | rabbitURI := os.Getenv(constants.RabbitMQ_URI) 117 | if elasticUrl != "" { 118 | cfg.RabbitMQ.URI = rabbitURI 119 | } 120 | 121 | return cfg, nil 122 | } 123 | -------------------------------------------------------------------------------- /config/config.yaml: -------------------------------------------------------------------------------- 1 | serviceName: search_service 2 | grpc: 3 | port: :5001 4 | development: true 5 | http: 6 | port: :8000 7 | development: true 8 | basePath: /api/v1 9 | productsPath: /api/v1/products 10 | debugErrorsResponse: true 11 | ignoreLogUrls: [ "metrics", "swagger" ] 12 | probes: 13 | readinessPath: /ready 14 | livenessPath: /live 15 | port: :3001 16 | pprof: :6001 17 | prometheusPath: /metrics 18 | prometheusPort: :8001 19 | checkIntervalSeconds: 10 20 | logger: 21 | level: debug 22 | devMode: false 23 | encoder: console 24 | jaeger: 25 | enable: true 26 | serviceName: search_service 27 | hostPort: "localhost:6831" 28 | logSpans: false 29 | timeouts: 30 | postgresInitMilliseconds: 1500 31 | postgresInitRetryCount: 3 32 | elasticSearch: 33 | addresses: [ "http://localhost:9200" ] 34 | username: "" 35 | password: "" 36 | apiKey: "" 37 | enableLogging: false 38 | elasticIndexes: 39 | products: 40 | path: config/mappings.json 41 | name: products 42 | alias: products-alias 43 | rabbitmq: 44 | uri: amqp://guest:guest@localhost:5672/ 45 | exchangeAndQueueBindings: 46 | indexProductBinding: 47 | exchangeName: products 48 | exchangeKind: direct 49 | queueName: index-product 50 | bindingKey: products-index 51 | concurrency: 10 52 | consumer: products-consumer 53 | bulkIndexer: 54 | numWorkers: 10 55 | flushBytes: 20000000 56 | flushIntervalSeconds: 15 57 | timeoutMilliseconds: 5000 58 | 59 | 60 | -------------------------------------------------------------------------------- /config/mappings.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "analysis": { 4 | "analyzer": { 5 | "autocomplete_analyzer": { 6 | "type": "custom", 7 | "tokenizer": "standard", 8 | "filter": [ 9 | "name_synonym_filter", 10 | "lowercase", 11 | "remove_duplicates", 12 | "ngram_filter" 13 | ] 14 | } 15 | }, 16 | "filter": { 17 | "ngram_filter": { 18 | "type": "edge_ngram", 19 | "min_gram": 1, 20 | "max_gram": 20 21 | }, 22 | "name_synonym_filter" : { 23 | "type": "synonym", 24 | "synonyms": [ 25 | "Water, water, вода, Вода, Цфеук, цфеук, Djlf, djlf", 26 | "Juice, juice, J7, сок, Сок, Огшсу, огшсу, Cjr, cjr", 27 | "Apple, apple, яблоко, Яблоко, z,kjrj, Z,kjrj, Фззду, фззду", 28 | "Orange, orange, Апельсинь, апельсин, Щкфтпу. щкфтпу, Fgtkmcby, fgtkmcby", 29 | "Cheery, cherry, Вишня, вишня, Сруккн, сруккн, Dbiyf, dbiyz", 30 | "Muffin, muffin, Маффин, маффин, cookies, Cookies, Ьгаашт, Ьгаашт, Vfaaby, vfaaby", 31 | "Tomato, tomato, Помидор, помидор, Томат, томат, Ещьфещ, ещьфещ, Njvfn, njvfn", 32 | "Chocolate, chocolate, Шоколад, шоколад, Срщсщдфеу, срщлщдфеу, Ijrjkfl, ijrjkfl" 33 | ] 34 | } 35 | } 36 | } 37 | }, 38 | "mappings": { 39 | "properties": { 40 | "title": { 41 | "type": "text", 42 | "analyzer": "autocomplete_analyzer", 43 | "search_analyzer" : "standard" 44 | }, 45 | "description": { 46 | "type": "text", 47 | "analyzer": "autocomplete_analyzer", 48 | "search_analyzer" : "standard" 49 | }, 50 | "image_url": { 51 | "type": "keyword" 52 | }, 53 | "count_in_stock": { 54 | "type": "long" 55 | }, 56 | "shop": { 57 | "type": "text", 58 | "analyzer": "autocomplete_analyzer", 59 | "search_analyzer" : "standard" 60 | }, 61 | "created_at": { 62 | "type": "date" 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /config/search_query.json: -------------------------------------------------------------------------------- 1 | { 2 | "query": { 3 | "bool": { 4 | "should": [ 5 | { 6 | "multi_match": { 7 | "query": "muf", 8 | "fields": [ 9 | "title", 10 | "description", 11 | "shop" 12 | ] 13 | } 14 | }, 15 | { 16 | "multi_match": { 17 | "query": "ьга", 18 | "fields": [ 19 | "title", 20 | "description", 21 | "shop" 22 | ] 23 | } 24 | } 25 | ], 26 | "must_not": [ 27 | { 28 | "range": { 29 | "count_in_stock": { 30 | "lte": 10 31 | } 32 | } 33 | } 34 | ] 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /config/translate.json: -------------------------------------------------------------------------------- 1 | { 2 | "q": "й", 3 | "й": "q", 4 | "w": "ц", 5 | "ц": "w", 6 | "e": "у", 7 | "у": "e", 8 | "r": "к", 9 | "к": "r", 10 | "t": "е", 11 | "е": "t", 12 | "y": "н", 13 | "н": "y", 14 | "u": "г", 15 | "г": "u", 16 | "i": "ш", 17 | "ш": "i", 18 | "o": "щ", 19 | "щ": "o", 20 | "p": "з", 21 | "з": "p", 22 | "[": "х", 23 | "х": "[", 24 | "a": "ф", 25 | "ф": "a", 26 | "s": "ы", 27 | "ы": "s", 28 | "d": "в", 29 | "в": "d", 30 | "f": "а", 31 | "а": "f", 32 | "g": "п", 33 | "п": "g", 34 | "h": "р", 35 | "р": "h", 36 | "j": "о", 37 | "о": "j", 38 | "k": "л", 39 | "л": "k", 40 | "l": "д", 41 | "д": "l", 42 | ";": "ж", 43 | "ж": ";", 44 | "'": "э", 45 | "э": "'", 46 | "z": "я", 47 | "я": "z", 48 | "x": "ч", 49 | "ч": "x", 50 | "c": "с", 51 | "с": "c", 52 | "v": "м", 53 | "м": "v", 54 | "b": "и", 55 | "и": "b", 56 | "n": "т", 57 | "т": "n", 58 | "m": "ь", 59 | "ь": "m", 60 | ",": "б", 61 | "б": ",", 62 | ".": "ю", 63 | "ю": ".", 64 | " ": " " 65 | } -------------------------------------------------------------------------------- /docker-compose.local.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | rabbitmq: 5 | image: rabbitmq:3.9-management-alpine 6 | container_name: rabbitmq 7 | restart: always 8 | ports: 9 | - "5672:5672" 10 | - "15672:15672" 11 | networks: [ "microservices" ] 12 | 13 | node01: 14 | image: docker.elastic.co/elasticsearch/elasticsearch:8.3.3 15 | container_name: node01 16 | restart: always 17 | environment: 18 | - node.name=node01 19 | - cluster.name=es-cluster-8 20 | - discovery.type=single-node 21 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 22 | - xpack.license.self_generated.type=basic 23 | - xpack.security.enabled=false 24 | ulimits: 25 | memlock: 26 | soft: -1 27 | hard: -1 28 | volumes: 29 | - ./es-data01:/usr/share/elasticsearch/data 30 | ports: 31 | - "9200:9200" 32 | - "9300:9300" 33 | networks: [ "microservices" ] 34 | 35 | kibana: 36 | image: docker.elastic.co/kibana/kibana:8.3.3 37 | restart: always 38 | environment: 39 | ELASTICSEARCH_HOSTS: http://node01:9200 40 | ports: 41 | - "5601:5601" 42 | depends_on: 43 | - node01 44 | networks: [ "microservices" ] 45 | 46 | jaeger: 47 | container_name: jaeger_container 48 | restart: always 49 | image: jaegertracing/all-in-one:1.35 50 | environment: 51 | - COLLECTOR_ZIPKIN_HTTP_PORT=9411 52 | ports: 53 | - "5775:5775/udp" 54 | - "6831:6831/udp" 55 | - "6832:6832/udp" 56 | - "5778:5778" 57 | - "16686:16686" 58 | - "14268:14268" 59 | - "14250:14250" 60 | - "9411:9411" 61 | networks: [ "microservices" ] 62 | 63 | prometheus: 64 | image: prom/prometheus:latest 65 | container_name: prometheus 66 | ports: 67 | - "9090:9090" 68 | command: 69 | - --config.file=/etc/prometheus/prometheus.yml 70 | volumes: 71 | - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro 72 | networks: [ "microservices" ] 73 | 74 | node_exporter: 75 | container_name: node_exporter_container 76 | restart: always 77 | image: prom/node-exporter 78 | ports: 79 | - '9101:9100' 80 | networks: [ "microservices" ] 81 | 82 | grafana: 83 | container_name: grafana_container 84 | restart: always 85 | image: grafana/grafana 86 | ports: 87 | - '3005:3000' 88 | networks: [ "microservices" ] 89 | 90 | volumes: 91 | es-data01: 92 | driver: local 93 | 94 | networks: 95 | microservices: 96 | name: microservices -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | microservice: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | container_name: search_microservice 9 | restart: always 10 | ports: 11 | - "8000:8000" 12 | - "3001:3001" 13 | environment: 14 | - JAEGER_HOST_PORT=host.docker.internal:6831 15 | - ELASTIC_URL=http://host.docker.internal:9200 16 | - RABBITMQ_URI=amqp://guest:guest@host.docker.internal:5672/ 17 | depends_on: 18 | - node01 19 | - grafana 20 | - rabbitmq 21 | - jaeger 22 | - node_exporter 23 | - prometheus 24 | networks: [ "microservices" ] 25 | 26 | rabbitmq: 27 | image: rabbitmq:3.9-management-alpine 28 | container_name: rabbitmq 29 | restart: always 30 | ports: 31 | - "5672:5672" 32 | - "15672:15672" 33 | networks: [ "microservices" ] 34 | 35 | node01: 36 | image: docker.elastic.co/elasticsearch/elasticsearch:8.3.3 37 | container_name: node01 38 | restart: always 39 | environment: 40 | - node.name=node01 41 | - cluster.name=es-cluster-8 42 | - discovery.type=single-node 43 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 44 | - xpack.license.self_generated.type=basic 45 | - xpack.security.enabled=false 46 | ulimits: 47 | memlock: 48 | soft: -1 49 | hard: -1 50 | volumes: 51 | - ./es-data01:/usr/share/elasticsearch/data 52 | ports: 53 | - "9200:9200" 54 | - "9300:9300" 55 | networks: [ "microservices" ] 56 | 57 | kibana: 58 | image: docker.elastic.co/kibana/kibana:8.3.3 59 | restart: always 60 | environment: 61 | ELASTICSEARCH_HOSTS: http://node01:9200 62 | ports: 63 | - "5601:5601" 64 | depends_on: 65 | - node01 66 | networks: [ "microservices" ] 67 | 68 | jaeger: 69 | container_name: jaeger_container 70 | restart: always 71 | image: jaegertracing/all-in-one:1.35 72 | environment: 73 | - COLLECTOR_ZIPKIN_HTTP_PORT=9411 74 | ports: 75 | - "5775:5775/udp" 76 | - "6831:6831/udp" 77 | - "6832:6832/udp" 78 | - "5778:5778" 79 | - "16686:16686" 80 | - "14268:14268" 81 | - "14250:14250" 82 | - "9411:9411" 83 | networks: [ "microservices" ] 84 | 85 | prometheus: 86 | image: prom/prometheus:latest 87 | container_name: prometheus 88 | ports: 89 | - "9090:9090" 90 | command: 91 | - --config.file=/etc/prometheus/prometheus.yml 92 | volumes: 93 | - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro 94 | networks: [ "microservices" ] 95 | 96 | node_exporter: 97 | container_name: node_exporter_container 98 | restart: always 99 | image: prom/node-exporter 100 | ports: 101 | - '9101:9100' 102 | networks: [ "microservices" ] 103 | 104 | grafana: 105 | container_name: grafana_container 106 | restart: always 107 | image: grafana/grafana 108 | ports: 109 | - '3005:3000' 110 | networks: [ "microservices" ] 111 | 112 | volumes: 113 | es-data01: 114 | driver: local 115 | 116 | networks: 117 | microservices: 118 | name: microservices -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AleksK1NG/go-elasticsearch 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/avast/retry-go v3.0.0+incompatible 7 | github.com/brianvoe/gofakeit/v6 v6.18.0 8 | github.com/elastic/elastic-transport-go/v8 v8.1.0 9 | github.com/elastic/go-elasticsearch/v8 v8.3.0 10 | github.com/go-playground/validator v9.31.0+incompatible 11 | github.com/go-resty/resty/v2 v2.7.0 12 | github.com/heptiolabs/healthcheck v0.0.0-20211123025425-613501dd5deb 13 | github.com/json-iterator/go v1.1.12 14 | github.com/labstack/echo/v4 v4.7.2 15 | github.com/opentracing/opentracing-go v1.2.0 16 | github.com/pkg/errors v0.9.1 17 | github.com/prometheus/client_golang v1.12.2 18 | github.com/rabbitmq/amqp091-go v1.4.0 19 | github.com/satori/go.uuid v1.2.0 20 | github.com/segmentio/kafka-go v0.4.33 21 | github.com/spf13/viper v1.12.0 22 | github.com/stretchr/testify v1.8.0 23 | github.com/uber/jaeger-client-go v2.30.0+incompatible 24 | go.uber.org/zap v1.21.0 25 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 26 | google.golang.org/grpc v1.46.2 27 | ) 28 | 29 | require ( 30 | github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect 31 | github.com/beorn7/perks v1.0.1 // indirect 32 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 33 | github.com/davecgh/go-spew v1.1.1 // indirect 34 | github.com/fsnotify/fsnotify v1.5.4 // indirect 35 | github.com/go-playground/locales v0.14.0 // indirect 36 | github.com/go-playground/universal-translator v0.18.0 // indirect 37 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 38 | github.com/golang/protobuf v1.5.2 // indirect 39 | github.com/hashicorp/hcl v1.0.0 // indirect 40 | github.com/klauspost/compress v1.15.7 // indirect 41 | github.com/labstack/gommon v0.3.1 // indirect 42 | github.com/leodido/go-urn v1.2.1 // indirect 43 | github.com/magiconair/properties v1.8.6 // indirect 44 | github.com/mattn/go-colorable v0.1.12 // indirect 45 | github.com/mattn/go-isatty v0.0.14 // indirect 46 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 47 | github.com/mitchellh/mapstructure v1.5.0 // indirect 48 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 49 | github.com/modern-go/reflect2 v1.0.2 // indirect 50 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 51 | github.com/pelletier/go-toml v1.9.5 // indirect 52 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 53 | github.com/pierrec/lz4/v4 v4.1.15 // indirect 54 | github.com/pmezard/go-difflib v1.0.0 // indirect 55 | github.com/prometheus/client_model v0.2.0 // indirect 56 | github.com/prometheus/common v0.32.1 // indirect 57 | github.com/prometheus/procfs v0.7.3 // indirect 58 | github.com/spf13/afero v1.8.2 // indirect 59 | github.com/spf13/cast v1.5.0 // indirect 60 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 61 | github.com/spf13/pflag v1.0.5 // indirect 62 | github.com/subosito/gotenv v1.3.0 // indirect 63 | github.com/uber/jaeger-lib v2.4.1+incompatible // indirect 64 | github.com/valyala/bytebufferpool v1.0.0 // indirect 65 | github.com/valyala/fasttemplate v1.2.1 // indirect 66 | go.uber.org/atomic v1.7.0 // indirect 67 | go.uber.org/multierr v1.6.0 // indirect 68 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect 69 | golang.org/x/net v0.0.0-20220706163947-c90051bbdb60 // indirect 70 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect 71 | golang.org/x/text v0.3.7 // indirect 72 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect 73 | google.golang.org/protobuf v1.28.0 // indirect 74 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 // indirect 75 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 76 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 77 | gopkg.in/ini.v1 v1.66.4 // indirect 78 | gopkg.in/yaml.v2 v2.4.0 // indirect 79 | gopkg.in/yaml.v3 v3.0.1 // indirect 80 | ) 81 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 28 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 29 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 30 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 31 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 32 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 33 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 34 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 35 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 37 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 39 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 40 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 41 | github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= 42 | github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= 43 | github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= 44 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 45 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 46 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 47 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 48 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 49 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 50 | github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= 51 | github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= 52 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 53 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 54 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 55 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 56 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 57 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 58 | github.com/brianvoe/gofakeit/v6 v6.18.0 h1:tDQ4zJVFQHaJKvY9xYSqGN4S7noZU/doFn15/aNbhCU= 59 | github.com/brianvoe/gofakeit/v6 v6.18.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= 60 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 61 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 62 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 63 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 64 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 65 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 66 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 67 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 68 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 69 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 70 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 71 | github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= 72 | github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 73 | github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 74 | github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 75 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 76 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 77 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 78 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 79 | github.com/elastic/elastic-transport-go/v8 v8.0.0-20211216131617-bbee439d559c/go.mod h1:87Tcz8IVNe6rVSLdBux1o/PEItLtyabHU3naC7IoqKI= 80 | github.com/elastic/elastic-transport-go/v8 v8.1.0 h1:NeqEz1ty4RQz+TVbUrpSU7pZ48XkzGWQj02k5koahIE= 81 | github.com/elastic/elastic-transport-go/v8 v8.1.0/go.mod h1:87Tcz8IVNe6rVSLdBux1o/PEItLtyabHU3naC7IoqKI= 82 | github.com/elastic/go-elasticsearch/v8 v8.3.0 h1:RF4iRbvWkiT6UksZ+OwSLeCEtBg/HO8r88xNiSmhb8U= 83 | github.com/elastic/go-elasticsearch/v8 v8.3.0/go.mod h1:Usvydt+x0dv9a1TzEUaovqbJor8rmOHy5dSmPeMAE2k= 84 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 85 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 86 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 87 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 88 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 89 | github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= 90 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 91 | github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 92 | github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= 93 | github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= 94 | github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= 95 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 96 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 97 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 98 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 99 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 100 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 101 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 102 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 103 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 104 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 105 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 106 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 107 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 108 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 109 | github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA= 110 | github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= 111 | github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= 112 | github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= 113 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 114 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 115 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 116 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 117 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 118 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 119 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 120 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 121 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 122 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 123 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 124 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 125 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 126 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 127 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 128 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 129 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 130 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 131 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 132 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 133 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 134 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 135 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 136 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 137 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 138 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 139 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 140 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 141 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 142 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 143 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 144 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 145 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 146 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 147 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 148 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 149 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 150 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 151 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 152 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 153 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 154 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 155 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 156 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 157 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 158 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 159 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 160 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 161 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 162 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 163 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 164 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 165 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 166 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 167 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 168 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 169 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 170 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 171 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 172 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 173 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 174 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 175 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 176 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 177 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 178 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 179 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 180 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 181 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 182 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 183 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 184 | github.com/heptiolabs/healthcheck v0.0.0-20211123025425-613501dd5deb h1:tsEKRC3PU9rMw18w/uAptoijhgG4EvlA5kfJPtwrMDk= 185 | github.com/heptiolabs/healthcheck v0.0.0-20211123025425-613501dd5deb/go.mod h1:NtmN9h8vrTveVQRLHcX2HQ5wIPBDCsZ351TGbZWgg38= 186 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 187 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 188 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 189 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 190 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 191 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 192 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 193 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 194 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 195 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 196 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 197 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 198 | github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 199 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 200 | github.com/klauspost/compress v1.15.7 h1:7cgTQxJCU/vy+oP/E3B9RGbQTgbiVzIJWIKOLoAsPok= 201 | github.com/klauspost/compress v1.15.7/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= 202 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 203 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 204 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 205 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 206 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 207 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 208 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 209 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 210 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 211 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 212 | github.com/labstack/echo/v4 v4.7.2 h1:Kv2/p8OaQ+M6Ex4eGimg9b9e6icoxA42JSlOR3msKtI= 213 | github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= 214 | github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o= 215 | github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= 216 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 217 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 218 | github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= 219 | github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= 220 | github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 221 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 222 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 223 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 224 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 225 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 226 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 227 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 228 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 229 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 230 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 231 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 232 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 233 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 234 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 235 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 236 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 237 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 238 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 239 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 240 | github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 241 | github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 242 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 243 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 244 | github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= 245 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= 246 | github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= 247 | github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 248 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 249 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 250 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 251 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 252 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 253 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 254 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 255 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 256 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 257 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 258 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 259 | github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= 260 | github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= 261 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 262 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 263 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 264 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 265 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 266 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 267 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 268 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 269 | github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= 270 | github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= 271 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 272 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 273 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 274 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 275 | github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= 276 | github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 277 | github.com/rabbitmq/amqp091-go v1.4.0 h1:T2G+J9W9OY4p64Di23J6yH7tOkMocgnESvYeBjuG9cY= 278 | github.com/rabbitmq/amqp091-go v1.4.0/go.mod h1:JsV0ofX5f1nwOGafb8L5rBItt9GyhfQfcJj+oyz0dGg= 279 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 280 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 281 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 282 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 283 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 284 | github.com/segmentio/kafka-go v0.4.33 h1:XHYuEifMYFVCU9A2p1wJprd7xHQKS+Sn6xgBr11+30k= 285 | github.com/segmentio/kafka-go v0.4.33/go.mod h1:GAjxBQJdQMB5zfNA21AhpaqOB2Mu+w3De4ni3Gbm8y0= 286 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 287 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 288 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 289 | github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= 290 | github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= 291 | github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= 292 | github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 293 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 294 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 295 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 296 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 297 | github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= 298 | github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= 299 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 300 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 301 | github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= 302 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 303 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 304 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 305 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 306 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 307 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 308 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 309 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 310 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 311 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 312 | github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= 313 | github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= 314 | github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= 315 | github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= 316 | github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= 317 | github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= 318 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 319 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 320 | github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= 321 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 322 | github.com/xdg/scram v1.0.5 h1:TuS0RFmt5Is5qm9Tm2SoD89OPqe4IRiFtyFY4iwWXsw= 323 | github.com/xdg/scram v1.0.5/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= 324 | github.com/xdg/stringprep v1.0.3 h1:cmL5Enob4W83ti/ZHuZLuKD/xqJfus4fVPwE+/BDm+4= 325 | github.com/xdg/stringprep v1.0.3/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= 326 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 327 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 328 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 329 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 330 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 331 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 332 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 333 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 334 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 335 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 336 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 337 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 338 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 339 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 340 | go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 341 | go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= 342 | go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 343 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 344 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 345 | go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= 346 | go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= 347 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 348 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 349 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 350 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 351 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 352 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 353 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 354 | golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 355 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= 356 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 357 | golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 358 | golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 359 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 360 | golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 361 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 362 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 363 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 364 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 365 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 366 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 367 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 368 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 369 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 370 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 371 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 372 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 373 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 374 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 375 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 376 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 377 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 378 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 379 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 380 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 381 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 382 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 383 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 384 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 385 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 386 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 387 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 388 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 389 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 390 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 391 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 392 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 393 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 394 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 395 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 396 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 397 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 398 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 399 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 400 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 401 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 402 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 403 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 404 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 405 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 406 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 407 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 408 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 409 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 410 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 411 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 412 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 413 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 414 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 415 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 416 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 417 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 418 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 419 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 420 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 421 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 422 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 423 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 424 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 425 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 426 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 427 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 428 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 429 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 430 | golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 431 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 432 | golang.org/x/net v0.0.0-20220706163947-c90051bbdb60 h1:8NSylCMxLW4JvserAndSgFL7aPli6A68yf0bYFTcWCM= 433 | golang.org/x/net v0.0.0-20220706163947-c90051bbdb60/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 434 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 435 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 436 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 437 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 438 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 439 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 440 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 441 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 442 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 443 | golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 444 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 445 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 446 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 447 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 448 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 449 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 450 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 451 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 452 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 453 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 454 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 455 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 456 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 457 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 458 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 459 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 460 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 461 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 462 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 463 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 464 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 465 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 466 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 467 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 468 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 469 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 470 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 471 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 472 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 473 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 474 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 475 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 476 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 477 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 478 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 479 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 480 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 481 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 482 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 483 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 484 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 485 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 486 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 487 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 488 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 489 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 490 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 491 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 492 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 493 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 494 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 495 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 496 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 497 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 498 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 499 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 500 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 501 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 502 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 503 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 504 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 505 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 506 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= 507 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 508 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 509 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 510 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 511 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 512 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 513 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 514 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 515 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 516 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 517 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 518 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 519 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 520 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 521 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 522 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= 523 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 524 | golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 525 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 526 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 527 | golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 528 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 529 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 530 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 531 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 532 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 533 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 534 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 535 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 536 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 537 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 538 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 539 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 540 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 541 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 542 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 543 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 544 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 545 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 546 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 547 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 548 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 549 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 550 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 551 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 552 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 553 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 554 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 555 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 556 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 557 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 558 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 559 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 560 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 561 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 562 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 563 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 564 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 565 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 566 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 567 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 568 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 569 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 570 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 571 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 572 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 573 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 574 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 575 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 576 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 577 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 578 | gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= 579 | gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= 580 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= 581 | gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= 582 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 583 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 584 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 585 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 586 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 587 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 588 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 589 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 590 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 591 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 592 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 593 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 594 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 595 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 596 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 597 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 598 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 599 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 600 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 601 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 602 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 603 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 604 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 605 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 606 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 607 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 608 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 609 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 610 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 611 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 612 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 613 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 614 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 615 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 616 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 617 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 618 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 619 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 620 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 621 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 622 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 623 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 624 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 625 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 626 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 627 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 628 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 629 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 630 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 631 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 632 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 633 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 634 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 635 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 636 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 637 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 638 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 639 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 640 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 641 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 642 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 643 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 644 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 645 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 646 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 647 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 648 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 649 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 650 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 651 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 652 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 653 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 654 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 655 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 656 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 657 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 658 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 659 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 660 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 661 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 662 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 663 | google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ= 664 | google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= 665 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 666 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 667 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 668 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 669 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 670 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 671 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 672 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 673 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 674 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 675 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 676 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 677 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 678 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 679 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 680 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY= 681 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= 682 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 683 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 684 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 685 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 686 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 687 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 688 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 689 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 690 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 691 | gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= 692 | gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 693 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 694 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 695 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 696 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 697 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 698 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 699 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 700 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 701 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 702 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 703 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 704 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 705 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 706 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 707 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 708 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 709 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 710 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 711 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 712 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 713 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 714 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 715 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 716 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 717 | -------------------------------------------------------------------------------- /internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "github.com/AleksK1NG/go-elasticsearch/config" 6 | "github.com/AleksK1NG/go-elasticsearch/internal/metrics" 7 | "github.com/AleksK1NG/go-elasticsearch/internal/product/delivery/http/v1" 8 | productRabbitConsumer "github.com/AleksK1NG/go-elasticsearch/internal/product/delivery/rabbitmq" 9 | "github.com/AleksK1NG/go-elasticsearch/internal/product/repository" 10 | "github.com/AleksK1NG/go-elasticsearch/internal/product/usecase" 11 | "github.com/AleksK1NG/go-elasticsearch/pkg/esclient" 12 | "github.com/AleksK1NG/go-elasticsearch/pkg/keyboard_manager" 13 | "github.com/AleksK1NG/go-elasticsearch/pkg/logger" 14 | "github.com/AleksK1NG/go-elasticsearch/pkg/middlewares" 15 | "github.com/AleksK1NG/go-elasticsearch/pkg/rabbitmq" 16 | "github.com/AleksK1NG/go-elasticsearch/pkg/tracing" 17 | "github.com/elastic/go-elasticsearch/v8" 18 | "github.com/go-playground/validator" 19 | "github.com/labstack/echo/v4" 20 | "github.com/opentracing/opentracing-go" 21 | amqp "github.com/rabbitmq/amqp091-go" 22 | "net/http" 23 | "os" 24 | "os/signal" 25 | "syscall" 26 | "time" 27 | ) 28 | 29 | const ( 30 | waitShotDownDuration = 3 * time.Second 31 | ) 32 | 33 | type app struct { 34 | log logger.Logger 35 | cfg *config.Config 36 | doneCh chan struct{} 37 | elasticClient *elasticsearch.Client 38 | echo *echo.Echo 39 | validate *validator.Validate 40 | middlewareManager middlewares.MiddlewareManager 41 | amqpConn *amqp.Connection 42 | amqpChan *amqp.Channel 43 | amqpPublisher rabbitmq.AmqpPublisher 44 | keyboardLayoutManager keyboard_manager.KeyboardLayoutManager 45 | metricsServer *echo.Echo 46 | healthCheckServer *http.Server 47 | metrics *metrics.SearchMicroserviceMetrics 48 | } 49 | 50 | func NewApp(log logger.Logger, cfg *config.Config) *app { 51 | return &app{log: log, cfg: cfg, validate: validator.New(), doneCh: make(chan struct{}), echo: echo.New()} 52 | } 53 | 54 | func (a *app) Run() error { 55 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGINT) 56 | defer cancel() 57 | 58 | if err := a.loadKeyMappings(); err != nil { 59 | return err 60 | } 61 | 62 | // enable tracing 63 | if a.cfg.Jaeger.Enable { 64 | tracer, closer, err := tracing.NewJaegerTracer(a.cfg.Jaeger) 65 | if err != nil { 66 | return err 67 | } 68 | defer closer.Close() // nolint: errcheck 69 | opentracing.SetGlobalTracer(tracer) 70 | } 71 | 72 | a.middlewareManager = middlewares.NewMiddlewareManager(a.log, a.cfg, a.getHttpMetricsCb()) 73 | a.metrics = metrics.NewSearchMicroserviceMetrics(a.cfg) 74 | 75 | if err := a.initRabbitMQ(ctx); err != nil { 76 | return err 77 | } 78 | defer a.closeRabbitConn() 79 | 80 | queue, err := rabbitmq.DeclareBinding(ctx, a.amqpChan, a.cfg.ExchangeAndQueueBindings.IndexProductBinding) 81 | if err != nil { 82 | return err 83 | } 84 | a.log.Infof("rabbitmq queue created: %+v for binding: %+v", queue, a.cfg.ExchangeAndQueueBindings.IndexProductBinding) 85 | 86 | if err := a.initRabbitMQPublisher(ctx); err != nil { 87 | return err 88 | } 89 | defer a.amqpPublisher.Close() 90 | 91 | if err := a.initElasticsearchClient(ctx); err != nil { 92 | return err 93 | } 94 | 95 | // connect elastic 96 | elasticInfoResponse, err := esclient.Info(ctx, a.elasticClient) 97 | if err != nil { 98 | return err 99 | } 100 | a.log.Infof("Elastic info response: %s", elasticInfoResponse.String()) 101 | 102 | if err := a.initIndexes(ctx); err != nil { 103 | return err 104 | } 105 | 106 | elasticRepository := repository.NewEsRepository(a.log, a.cfg, a.elasticClient, a.keyboardLayoutManager) 107 | productUseCase := usecase.NewProductUseCase(a.log, a.cfg, elasticRepository, a.amqpPublisher) 108 | productController := v1.NewProductController(a.log, a.cfg, productUseCase, a.echo.Group(a.cfg.Http.ProductsPath), a.validate, a.metrics) 109 | productController.MapRoutes() 110 | 111 | go func() { 112 | if err := a.runHttpServer(); err != nil { 113 | a.log.Errorf("(runHttpServer) err: %v", err) 114 | cancel() 115 | } 116 | }() 117 | a.log.Infof("%s is listening on PORT: %v", GetMicroserviceName(a.cfg), a.cfg.Http.Port) 118 | 119 | productConsumer := productRabbitConsumer.NewConsumer(a.log, a.cfg, a.amqpConn, a.amqpChan, productUseCase, a.elasticClient, a.metrics) 120 | if err := productConsumer.InitBulkIndexer(); err != nil { 121 | a.log.Errorf("(InitBulkIndexer) err: %v", err) 122 | cancel() 123 | } 124 | defer productConsumer.Close(ctx) 125 | 126 | go func() { 127 | if err := rabbitmq.ConsumeQueue( 128 | ctx, 129 | a.amqpChan, 130 | a.cfg.ExchangeAndQueueBindings.IndexProductBinding.Concurrency, 131 | a.cfg.ExchangeAndQueueBindings.IndexProductBinding.QueueName, 132 | a.cfg.ExchangeAndQueueBindings.IndexProductBinding.Consumer, 133 | productConsumer.ConsumeIndexDeliveries, 134 | ); err != nil { 135 | a.log.Errorf("(rabbitmq.ConsumeQueue) err: %v", err) 136 | cancel() 137 | } 138 | }() 139 | 140 | a.runMetrics(cancel) 141 | a.runHealthCheck(ctx) 142 | 143 | <-ctx.Done() 144 | a.waitShootDown(waitShotDownDuration) 145 | 146 | if err := a.echo.Shutdown(ctx); err != nil { 147 | a.log.Warnf("(Shutdown) err: %v", err) 148 | } 149 | if err := a.metricsServer.Shutdown(ctx); err != nil { 150 | a.log.Warnf("(Shutdown) metricsServer err: %v", err) 151 | } 152 | if err := a.shutDownHealthCheckServer(ctx); err != nil { 153 | a.log.Warnf("(shutDownHealthCheckServer) HealthCheckServer err: %v", err) 154 | } 155 | 156 | <-a.doneCh 157 | a.log.Infof("%s app exited properly", GetMicroserviceName(a.cfg)) 158 | return nil 159 | } 160 | -------------------------------------------------------------------------------- /internal/app/elastic.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "github.com/AleksK1NG/go-elasticsearch/pkg/elastic" 6 | "github.com/avast/retry-go" 7 | "github.com/pkg/errors" 8 | "time" 9 | ) 10 | 11 | const ( 12 | initElasticsearchClientAttempts = 5 13 | initElasticsearchClientDelay = time.Duration(1500) * time.Millisecond 14 | ) 15 | 16 | func (a *app) initElasticsearchClient(ctx context.Context) error { 17 | retryOptions := []retry.Option{ 18 | retry.Attempts(initElasticsearchClientAttempts), 19 | retry.Delay(initElasticsearchClientDelay), 20 | retry.DelayType(retry.BackOffDelay), 21 | retry.LastErrorOnly(true), 22 | retry.Context(ctx), 23 | retry.OnRetry(func(n uint, err error) { 24 | a.log.Errorf("retry connect elasticsearch err: %v", err) 25 | }), 26 | } 27 | 28 | return retry.Do(func() error { 29 | elasticSearchClient, err := elastic.NewElasticSearchClient(a.cfg.ElasticSearch) 30 | if err != nil { 31 | return errors.Wrap(err, "NewElasticSearchClient") 32 | } 33 | a.elasticClient = elasticSearchClient 34 | return nil 35 | }, retryOptions...) 36 | } 37 | -------------------------------------------------------------------------------- /internal/app/healthcheck.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "github.com/AleksK1NG/go-elasticsearch/pkg/constants" 6 | "github.com/AleksK1NG/go-elasticsearch/pkg/esclient" 7 | "github.com/heptiolabs/healthcheck" 8 | "github.com/pkg/errors" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | func (a *app) runHealthCheck(ctx context.Context) { 14 | health := healthcheck.NewHandler() 15 | 16 | mux := http.NewServeMux() 17 | mux.HandleFunc(a.cfg.Probes.LivenessPath, health.LiveEndpoint) 18 | mux.HandleFunc(a.cfg.Probes.ReadinessPath, health.ReadyEndpoint) 19 | 20 | a.healthCheckServer = &http.Server{ 21 | Handler: mux, 22 | Addr: a.cfg.Probes.Port, 23 | WriteTimeout: writeTimeout, 24 | ReadTimeout: readTimeout, 25 | } 26 | 27 | a.configureHealthCheckEndpoints(ctx, health) 28 | 29 | go func() { 30 | a.log.Infof("(%s) Kubernetes probes listening on port: %s", a.cfg.ServiceName, a.cfg.Probes.Port) 31 | if err := a.healthCheckServer.ListenAndServe(); err != nil { 32 | a.log.Errorf("(ListenAndServe) err: %v", err) 33 | } 34 | }() 35 | } 36 | 37 | func (a *app) configureHealthCheckEndpoints(ctx context.Context, health healthcheck.Handler) { 38 | 39 | health.AddReadinessCheck(constants.ElasticSearch, healthcheck.AsyncWithContext(ctx, func() error { 40 | _, err := esclient.Info(ctx, a.elasticClient) 41 | if err != nil { 42 | a.log.Warnf("(ElasticSearch Readiness Check) err: %v", err) 43 | return errors.Wrap(err, "esClient.Info") 44 | } 45 | return nil 46 | }, time.Duration(a.cfg.Probes.CheckIntervalSeconds)*time.Second)) 47 | 48 | health.AddLivenessCheck(constants.ElasticSearch, healthcheck.AsyncWithContext(ctx, func() error { 49 | _, err := esclient.Info(ctx, a.elasticClient) 50 | if err != nil { 51 | a.log.Warnf("(ElasticSearch Liveness Check) err: %v", err) 52 | return errors.Wrap(err, "esClient.Info") 53 | } 54 | return nil 55 | }, time.Duration(a.cfg.Probes.CheckIntervalSeconds)*time.Second)) 56 | 57 | health.AddLivenessCheck(constants.RabbitMQ, healthcheck.AsyncWithContext(ctx, func() error { 58 | if a.amqpConn.IsClosed() || a.amqpChan.IsClosed() { 59 | a.log.Warnf("(rabbitmq Liveness Check) err: %v", errors.New("amqp conn closed")) 60 | return errors.New("amqp conn closed") 61 | } 62 | return nil 63 | }, time.Duration(a.cfg.Probes.CheckIntervalSeconds)*time.Second)) 64 | 65 | health.AddReadinessCheck(constants.RabbitMQ, healthcheck.AsyncWithContext(ctx, func() error { 66 | if a.amqpConn.IsClosed() || a.amqpChan.IsClosed() { 67 | a.log.Warnf("(rabbitmq Readiness Check) err: %v", errors.New("amqp conn closed")) 68 | return errors.New("amqp conn closed") 69 | } 70 | return nil 71 | }, time.Duration(a.cfg.Probes.CheckIntervalSeconds)*time.Second)) 72 | } 73 | 74 | func (a *app) shutDownHealthCheckServer(ctx context.Context) error { 75 | return a.healthCheckServer.Shutdown(ctx) 76 | } 77 | -------------------------------------------------------------------------------- /internal/app/http_server.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "github.com/labstack/echo/v4/middleware" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | const ( 11 | maxHeaderBytes = 1 << 20 12 | stackSize = 1 << 10 // 1 KB 13 | bodyLimit = "2M" 14 | readTimeout = 15 * time.Second 15 | writeTimeout = 15 * time.Second 16 | gzipLevel = 5 17 | ) 18 | 19 | func (a *app) runHttpServer() error { 20 | a.mapRoutes() 21 | 22 | a.echo.Server.ReadTimeout = readTimeout 23 | a.echo.Server.WriteTimeout = writeTimeout 24 | a.echo.Server.MaxHeaderBytes = maxHeaderBytes 25 | 26 | return a.echo.Start(a.cfg.Http.Port) 27 | } 28 | 29 | func (a *app) mapRoutes() { 30 | a.echo.Use(a.middlewareManager.RequestLoggerMiddleware) 31 | a.echo.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ 32 | StackSize: stackSize, 33 | DisablePrintStack: false, 34 | DisableStackAll: false, 35 | })) 36 | a.echo.Use(middleware.RequestID()) 37 | a.echo.Use(middleware.GzipWithConfig(middleware.GzipConfig{ 38 | Level: gzipLevel, 39 | Skipper: func(c echo.Context) bool { 40 | return strings.Contains(c.Request().URL.Path, "swagger") 41 | }, 42 | })) 43 | a.echo.Use(middleware.BodyLimit(bodyLimit)) 44 | } 45 | -------------------------------------------------------------------------------- /internal/app/keymappings.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "github.com/pkg/errors" 4 | 5 | func (a *app) loadKeyMappings() error { 6 | keyboardLayoutManager, err := a.loadKeyboardLayoutManager() 7 | if err != nil { 8 | a.log.Errorf("loadKeyMappings keyboardLayoutManager err: %v", err) 9 | return errors.Wrap(err, "loadKeyboardLayoutManager") 10 | } 11 | a.keyboardLayoutManager = keyboardLayoutManager 12 | return nil 13 | } 14 | -------------------------------------------------------------------------------- /internal/app/metrics.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "github.com/labstack/echo/v4" 6 | "github.com/labstack/echo/v4/middleware" 7 | "github.com/prometheus/client_golang/prometheus/promhttp" 8 | ) 9 | 10 | func (a *app) runMetrics(cancel context.CancelFunc) { 11 | a.metricsServer = echo.New() 12 | go func() { 13 | a.metricsServer.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ 14 | StackSize: stackSize, 15 | DisablePrintStack: false, 16 | DisableStackAll: false, 17 | })) 18 | 19 | a.metricsServer.GET(a.cfg.Probes.PrometheusPath, echo.WrapHandler(promhttp.Handler())) 20 | a.log.Infof("Metrics app is running on port: %s", a.cfg.Probes.PrometheusPort) 21 | if err := a.metricsServer.Start(a.cfg.Probes.PrometheusPort); err != nil { 22 | a.log.Errorf("metricsServer.Start: %v", err) 23 | cancel() 24 | } 25 | }() 26 | } 27 | -------------------------------------------------------------------------------- /internal/app/rabbitmq.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "github.com/AleksK1NG/go-elasticsearch/pkg/rabbitmq" 6 | "github.com/avast/retry-go" 7 | "github.com/pkg/errors" 8 | "time" 9 | ) 10 | 11 | const ( 12 | prefetchCount = 1 13 | prefetchSize = 0 14 | qosGlobal = true 15 | initRabbitMQAttempts = 5 16 | initRabbitMQDelay = time.Duration(1500) * time.Millisecond 17 | ) 18 | 19 | func (a *app) initRabbitMQ(ctx context.Context) error { 20 | retryOptions := []retry.Option{ 21 | retry.Attempts(initRabbitMQAttempts), 22 | retry.Delay(initRabbitMQDelay), 23 | retry.DelayType(retry.BackOffDelay), 24 | retry.LastErrorOnly(true), 25 | retry.Context(ctx), 26 | retry.OnRetry(func(n uint, err error) { 27 | a.log.Errorf("retry connect rabbitmq err: %v", err) 28 | }), 29 | } 30 | 31 | return retry.Do(func() error { 32 | amqpConn, err := rabbitmq.NewRabbitMQConnection(a.cfg.RabbitMQ) 33 | if err != nil { 34 | return errors.Wrap(err, "rabbitmq.NewRabbitMQConnection") 35 | } 36 | a.amqpConn = amqpConn 37 | 38 | amqpChan, err := amqpConn.Channel() 39 | if err != nil { 40 | return errors.Wrap(err, "amqpConn.Channel") 41 | } 42 | a.amqpChan = amqpChan 43 | 44 | if err := a.amqpChan.Qos(prefetchCount, prefetchSize, qosGlobal); err != nil { 45 | a.log.Errorf("amqpChan.Qos err: %v", err) 46 | return errors.Wrap(err, "amqpChan.Qos") 47 | } 48 | 49 | a.log.Infof("rabbitmq connected: %+v", a.amqpConn) 50 | return nil 51 | }, retryOptions...) 52 | } 53 | 54 | func (a *app) closeRabbitConn() { 55 | if err := a.amqpChan.Close(); err != nil { 56 | a.log.Errorf("amqpChan.Close err: %v", err) 57 | } 58 | if err := a.amqpConn.Close(); err != nil { 59 | a.log.Errorf("amqpConn.Close err: %v", err) 60 | } 61 | } 62 | 63 | func (a *app) initRabbitMQPublisher(ctx context.Context) error { 64 | retryOptions := []retry.Option{ 65 | retry.Attempts(initRabbitMQAttempts), 66 | retry.Delay(initRabbitMQDelay), 67 | retry.DelayType(retry.BackOffDelay), 68 | retry.LastErrorOnly(true), 69 | retry.Context(ctx), 70 | retry.OnRetry(func(n uint, err error) { 71 | a.log.Errorf("retry connect rabbitmq publisher err: %v", err) 72 | }), 73 | } 74 | 75 | return retry.Do(func() error { 76 | amqpPublisher, err := rabbitmq.NewPublisher(a.cfg.RabbitMQ, a.log) 77 | if err != nil { 78 | return errors.Wrap(err, "rabbitmq.NewPublisher") 79 | } 80 | a.amqpPublisher = amqpPublisher 81 | return nil 82 | 83 | }, retryOptions...) 84 | } 85 | -------------------------------------------------------------------------------- /internal/app/utils.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/AleksK1NG/go-elasticsearch/config" 7 | "github.com/AleksK1NG/go-elasticsearch/pkg/esclient" 8 | "github.com/AleksK1NG/go-elasticsearch/pkg/keyboard_manager" 9 | "github.com/AleksK1NG/go-elasticsearch/pkg/middlewares" 10 | "github.com/AleksK1NG/go-elasticsearch/pkg/serializer" 11 | "github.com/pkg/errors" 12 | "io" 13 | "os" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | func (a *app) waitShootDown(duration time.Duration) { 19 | go func() { 20 | time.Sleep(duration) 21 | a.doneCh <- struct{}{} 22 | }() 23 | } 24 | 25 | func GetMicroserviceName(cfg *config.Config) string { 26 | return fmt.Sprintf("(%s)", strings.ToUpper(cfg.ServiceName)) 27 | } 28 | 29 | func (a *app) initIndexes(ctx context.Context) error { 30 | exists, err := a.isIndexExists(ctx, a.cfg.ElasticIndexes.ProductsIndex.Name) 31 | if err != nil { 32 | return err 33 | } 34 | if !exists { 35 | if err := a.uploadElasticMappings(ctx, a.cfg.ElasticIndexes.ProductsIndex); err != nil { 36 | return err 37 | } 38 | } 39 | a.log.Infof("index exists: %+v", a.cfg.ElasticIndexes.ProductsIndex) 40 | 41 | response, err := a.elasticClient.Indices.PutAlias( 42 | []string{a.cfg.ElasticIndexes.ProductsIndex.Name}, 43 | a.cfg.ElasticIndexes.ProductsIndex.Alias, 44 | a.elasticClient.Indices.PutAlias.WithContext(ctx), 45 | a.elasticClient.Indices.PutAlias.WithHuman(), 46 | a.elasticClient.Indices.PutAlias.WithPretty(), 47 | a.elasticClient.Indices.PutAlias.WithTimeout(5*time.Second), 48 | ) 49 | if err != nil { 50 | a.log.Errorf("Indices.PutAlias err: %v", err) 51 | return err 52 | } 53 | defer response.Body.Close() 54 | 55 | if response.IsError() { 56 | a.log.Errorf("create alias error: %s", response.String()) 57 | return errors.Wrap(errors.New(response.String()), "response.IsError") 58 | } 59 | 60 | a.log.Infof("alias: %s for indexes: %+v is created", a.cfg.ElasticIndexes.ProductsIndex.Alias, []string{a.cfg.ElasticIndexes.ProductsIndex.Name}) 61 | return nil 62 | } 63 | 64 | func (a *app) isIndexExists(ctx context.Context, indexName string) (bool, error) { 65 | response, err := esclient.Exists(ctx, a.elasticClient, []string{indexName}) 66 | if err != nil { 67 | a.log.Errorf("initIndexes err: %v", err) 68 | return false, errors.Wrap(err, "esclient.Exists") 69 | } 70 | defer response.Body.Close() 71 | 72 | if response.IsError() && response.StatusCode == 404 { 73 | return false, nil 74 | } 75 | 76 | a.log.Infof("exists response: %s", response) 77 | return true, nil 78 | } 79 | 80 | func (a *app) uploadElasticMappings(ctx context.Context, indexConfig esclient.ElasticIndex) error { 81 | getWd, err := os.Getwd() 82 | if err != nil { 83 | return errors.Wrap(err, "os.getWd") 84 | } 85 | path := fmt.Sprintf("%s/%s", getWd, indexConfig.Path) 86 | 87 | mappingsFile, err := os.Open(path) 88 | if err != nil { 89 | return err 90 | } 91 | defer mappingsFile.Close() 92 | 93 | mappingsBytes, err := io.ReadAll(mappingsFile) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | a.log.Infof("loaded mappings bytes: %s", string(mappingsBytes)) 99 | 100 | response, err := esclient.CreateIndex(ctx, a.elasticClient, indexConfig.Name, mappingsBytes) 101 | if err != nil { 102 | return err 103 | } 104 | defer response.Body.Close() 105 | 106 | if response.IsError() && response.StatusCode != 400 { 107 | return errors.New(fmt.Sprintf("err init index: %s", response.String())) 108 | } 109 | 110 | a.log.Infof("created index: %s", response.String()) 111 | return nil 112 | } 113 | 114 | func (a *app) loadKeyboardLayoutManager() (keyboard_manager.KeyboardLayoutManager, error) { 115 | getWd, err := os.Getwd() 116 | if err != nil { 117 | return nil, errors.Wrap(err, "os.getWd") 118 | } 119 | 120 | keysJsonPathFile, err := os.Open(fmt.Sprintf("%s/config/translate.json", getWd)) 121 | if err != nil { 122 | return nil, err 123 | } 124 | defer keysJsonPathFile.Close() 125 | 126 | keysJsonBytes, err := io.ReadAll(keysJsonPathFile) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | a.log.Infof("keys mappings: %s", string(keysJsonBytes)) 132 | 133 | keyMappings := map[string]string{} 134 | if err := serializer.Unmarshal(keysJsonBytes, &keyMappings); err != nil { 135 | return nil, err 136 | } 137 | a.log.Infof("keys mappings data: %+v", keyMappings) 138 | 139 | return keyboard_manager.NewKeyboardLayoutManager(a.log, keyMappings), nil 140 | } 141 | 142 | func (a *app) getHttpMetricsCb() middlewares.MiddlewareMetricsCb { 143 | return func(err error) { 144 | if err != nil { 145 | a.metrics.ErrorHttpRequests.Inc() 146 | } else { 147 | a.metrics.SuccessHttpRequests.Inc() 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /internal/dto/search_producrs_response.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/AleksK1NG/go-elasticsearch/internal/product/domain" 5 | "github.com/AleksK1NG/go-elasticsearch/pkg/utils" 6 | ) 7 | 8 | type SearchProductsResponse struct { 9 | SearchTerm string `json:"searchTerm"` 10 | Pagination *utils.PaginationResponse `json:"pagination"` 11 | Products []*domain.Product `json:"products"` 12 | } 13 | -------------------------------------------------------------------------------- /internal/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "fmt" 5 | "github.com/AleksK1NG/go-elasticsearch/config" 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/prometheus/client_golang/prometheus/promauto" 8 | ) 9 | 10 | type SearchMicroserviceMetrics struct { 11 | SuccessHttpRequests prometheus.Counter 12 | ErrorHttpRequests prometheus.Counter 13 | 14 | HttpSuccessIndexAsyncRequests prometheus.Counter 15 | HttpSuccessIndexRequests prometheus.Counter 16 | HttpSuccessSearchRequests prometheus.Counter 17 | 18 | RabbitMQSuccessBatchInsertMessages prometheus.Counter 19 | } 20 | 21 | func NewSearchMicroserviceMetrics(cfg *config.Config) *SearchMicroserviceMetrics { 22 | return &SearchMicroserviceMetrics{ 23 | 24 | SuccessHttpRequests: promauto.NewCounter(prometheus.CounterOpts{ 25 | Name: fmt.Sprintf("%s_success_http_requests_total", cfg.ServiceName), 26 | Help: "The total number of success http requests", 27 | }), 28 | ErrorHttpRequests: promauto.NewCounter(prometheus.CounterOpts{ 29 | Name: fmt.Sprintf("%s_error_http_requests_total", cfg.ServiceName), 30 | Help: "The total number of error http requests", 31 | }), 32 | HttpSuccessIndexRequests: promauto.NewCounter(prometheus.CounterOpts{ 33 | Name: fmt.Sprintf("%s_success_http_index_requests_total", cfg.ServiceName), 34 | Help: "The total number of success_http_index_requests_total requests", 35 | }), 36 | HttpSuccessIndexAsyncRequests: promauto.NewCounter(prometheus.CounterOpts{ 37 | Name: fmt.Sprintf("%s_success_http_index_async_requests_total", cfg.ServiceName), 38 | Help: "The total number of success_http_index_async_requests_total requests", 39 | }), 40 | HttpSuccessSearchRequests: promauto.NewCounter(prometheus.CounterOpts{ 41 | Name: fmt.Sprintf("%s_success_http_search_requests_total", cfg.ServiceName), 42 | Help: "The total number of success_http_search_requests_total requests", 43 | }), 44 | RabbitMQSuccessBatchInsertMessages: promauto.NewCounter(prometheus.CounterOpts{ 45 | Name: fmt.Sprintf("%s_success_rabbitmq_batch_insert_messages_total", cfg.ServiceName), 46 | Help: "The total number of success_rabbitmq_batch_insert_messages requests", 47 | }), 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/product/delivery/http/v1/controller.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/AleksK1NG/go-elasticsearch/config" 5 | "github.com/AleksK1NG/go-elasticsearch/internal/dto" 6 | "github.com/AleksK1NG/go-elasticsearch/internal/metrics" 7 | "github.com/AleksK1NG/go-elasticsearch/internal/product/domain" 8 | "github.com/AleksK1NG/go-elasticsearch/pkg/constants" 9 | httpErrors "github.com/AleksK1NG/go-elasticsearch/pkg/http_errors" 10 | "github.com/AleksK1NG/go-elasticsearch/pkg/logger" 11 | "github.com/AleksK1NG/go-elasticsearch/pkg/tracing" 12 | "github.com/AleksK1NG/go-elasticsearch/pkg/utils" 13 | "github.com/go-playground/validator" 14 | "github.com/labstack/echo/v4" 15 | "github.com/opentracing/opentracing-go/log" 16 | uuid "github.com/satori/go.uuid" 17 | "net/http" 18 | ) 19 | 20 | type productController struct { 21 | log logger.Logger 22 | cfg *config.Config 23 | productUseCase domain.ProductUseCase 24 | group *echo.Group 25 | validate *validator.Validate 26 | metrics *metrics.SearchMicroserviceMetrics 27 | } 28 | 29 | func NewProductController( 30 | log logger.Logger, 31 | cfg *config.Config, 32 | productUseCase domain.ProductUseCase, 33 | group *echo.Group, 34 | validate *validator.Validate, 35 | metrics *metrics.SearchMicroserviceMetrics, 36 | ) *productController { 37 | return &productController{log: log, cfg: cfg, productUseCase: productUseCase, group: group, validate: validate, metrics: metrics} 38 | } 39 | 40 | func (h *productController) indexAsync() echo.HandlerFunc { 41 | return func(c echo.Context) error { 42 | ctx, span := tracing.StartHttpServerTracerSpan(c, "productController.indexAsync") 43 | defer span.Finish() 44 | 45 | var product domain.Product 46 | if err := c.Bind(&product); err != nil { 47 | h.log.Errorf("(Bind) err: %v", tracing.TraceWithErr(span, err)) 48 | return httpErrors.ErrorCtxResponse(c, err, h.cfg.Http.DebugErrorsResponse) 49 | } 50 | product.ID = uuid.NewV4().String() 51 | 52 | if err := h.productUseCase.IndexAsync(ctx, product); err != nil { 53 | h.log.Errorf("(productUseCase.IndexAsync) err: %v", tracing.TraceWithErr(span, err)) 54 | return httpErrors.ErrorCtxResponse(c, err, h.cfg.Http.DebugErrorsResponse) 55 | } 56 | 57 | h.log.Infof("created product: %+v", product) 58 | h.metrics.HttpSuccessIndexAsyncRequests.Inc() 59 | return c.JSON(http.StatusCreated, product) 60 | } 61 | } 62 | 63 | func (h *productController) index() echo.HandlerFunc { 64 | return func(c echo.Context) error { 65 | ctx, span := tracing.StartHttpServerTracerSpan(c, "productController.index") 66 | defer span.Finish() 67 | 68 | var product domain.Product 69 | if err := c.Bind(&product); err != nil { 70 | h.log.Errorf("(Bind) err: %v", tracing.TraceWithErr(span, err)) 71 | return httpErrors.ErrorCtxResponse(c, err, h.cfg.Http.DebugErrorsResponse) 72 | } 73 | product.ID = uuid.NewV4().String() 74 | 75 | if err := h.productUseCase.Index(ctx, product); err != nil { 76 | h.log.Errorf("(productUseCase.Index) err: %v", tracing.TraceWithErr(span, err)) 77 | return httpErrors.ErrorCtxResponse(c, err, h.cfg.Http.DebugErrorsResponse) 78 | } 79 | 80 | h.log.Infof("created product: %+v", product) 81 | h.metrics.HttpSuccessIndexRequests.Inc() 82 | return c.JSON(http.StatusCreated, product) 83 | } 84 | } 85 | 86 | func (h *productController) search() echo.HandlerFunc { 87 | return func(c echo.Context) error { 88 | ctx, span := tracing.StartHttpServerTracerSpan(c, "productController.search") 89 | defer span.Finish() 90 | 91 | searchTerm := c.QueryParam("search") 92 | pagination := utils.NewPaginationFromQueryParams(c.QueryParam(constants.Size), c.QueryParam(constants.Page)) 93 | 94 | searchResult, err := h.productUseCase.Search(ctx, searchTerm, pagination) 95 | if err != nil { 96 | h.log.Errorf("(productUseCase.Search) err: %v", tracing.TraceWithErr(span, err)) 97 | return httpErrors.ErrorCtxResponse(c, err, h.cfg.Http.DebugErrorsResponse) 98 | } 99 | 100 | h.log.Infof("search result: %s", searchResult.PaginationResponse.String()) 101 | h.metrics.HttpSuccessSearchRequests.Inc() 102 | span.LogFields(log.String("search result", searchResult.PaginationResponse.String())) 103 | return c.JSON(http.StatusOK, dto.SearchProductsResponse{ 104 | SearchTerm: searchTerm, 105 | Pagination: searchResult.PaginationResponse, 106 | Products: searchResult.List, 107 | }) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /internal/product/delivery/http/v1/controller_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "github.com/AleksK1NG/go-elasticsearch/internal/product/domain" 6 | "github.com/AleksK1NG/go-elasticsearch/pkg/http_client" 7 | "github.com/brianvoe/gofakeit/v6" 8 | uuid "github.com/satori/go.uuid" 9 | "github.com/stretchr/testify/require" 10 | "golang.org/x/sync/errgroup" 11 | "net/http" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func TestIndexAsync(t *testing.T) { 17 | 18 | t.Parallel() 19 | 20 | client := http_client.NewHttpClient(true) 21 | 22 | ctx, cancel := context.WithTimeout(context.Background(), 165*time.Second) 23 | defer cancel() 24 | g, ctx := errgroup.WithContext(ctx) 25 | 26 | for i := 0; i < 10; i++ { 27 | g.Go(func() error { 28 | for j := 0; j < 500; j++ { 29 | product := domain.Product{ 30 | ID: uuid.NewV4().String(), 31 | Title: gofakeit.Breakfast(), 32 | Description: gofakeit.LoremIpsumSentence(60), 33 | ImageURL: gofakeit.URL(), 34 | CountInStock: gofakeit.Int64(), 35 | Shop: gofakeit.Company(), 36 | CreatedAt: time.Now().UTC(), 37 | } 38 | 39 | t.Logf("product: %+v", product) 40 | 41 | response, err := client.R(). 42 | SetContext(ctx). 43 | SetBody(product). 44 | Post("http://localhost:8000/api/v1/products/async") 45 | if err != nil { 46 | return err 47 | } 48 | t.Logf("response: %s", response.String()) 49 | } 50 | return nil 51 | }) 52 | } 53 | 54 | err := g.Wait() 55 | require.NoError(t, err) 56 | } 57 | 58 | func TestIndexProduct(t *testing.T) { 59 | 60 | t.Parallel() 61 | 62 | client := http_client.NewHttpClient(true) 63 | 64 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 65 | defer cancel() 66 | 67 | product := domain.Product{ 68 | ID: uuid.NewV4().String(), 69 | Title: gofakeit.Breakfast(), 70 | Description: gofakeit.LoremIpsumSentence(60), 71 | ImageURL: gofakeit.URL(), 72 | CountInStock: gofakeit.Int64(), 73 | Shop: gofakeit.Company(), 74 | CreatedAt: time.Now().UTC(), 75 | } 76 | 77 | t.Logf("product id: %s", product.ID) 78 | 79 | response, err := client.R(). 80 | SetContext(ctx). 81 | SetBody(product). 82 | Post("http://localhost:8000/api/v1/products") 83 | require.NoError(t, err) 84 | require.NotNil(t, response) 85 | require.False(t, response.IsError()) 86 | require.True(t, response.IsSuccess()) 87 | require.Equal(t, response.StatusCode(), http.StatusCreated) 88 | } 89 | -------------------------------------------------------------------------------- /internal/product/delivery/http/v1/routes.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | func (h *productController) MapRoutes() { 4 | h.group.POST("", h.index()) 5 | h.group.POST("/async", h.indexAsync()) 6 | h.group.GET("/search", h.search()) 7 | } 8 | -------------------------------------------------------------------------------- /internal/product/delivery/rabbitmq/consumer.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "github.com/AleksK1NG/go-elasticsearch/config" 7 | "github.com/AleksK1NG/go-elasticsearch/internal/metrics" 8 | "github.com/AleksK1NG/go-elasticsearch/internal/product/domain" 9 | "github.com/AleksK1NG/go-elasticsearch/pkg/logger" 10 | "github.com/AleksK1NG/go-elasticsearch/pkg/serializer" 11 | "github.com/AleksK1NG/go-elasticsearch/pkg/tracing" 12 | "github.com/elastic/go-elasticsearch/v8" 13 | "github.com/elastic/go-elasticsearch/v8/esutil" 14 | "github.com/opentracing/opentracing-go" 15 | "github.com/pkg/errors" 16 | amqp "github.com/rabbitmq/amqp091-go" 17 | "time" 18 | ) 19 | 20 | type consumer struct { 21 | log logger.Logger 22 | cfg *config.Config 23 | amqpConn *amqp.Connection 24 | amqpChan *amqp.Channel 25 | productUseCase domain.ProductUseCase 26 | bulkIndexer esutil.BulkIndexer 27 | esClient *elasticsearch.Client 28 | metrics *metrics.SearchMicroserviceMetrics 29 | } 30 | 31 | func NewConsumer( 32 | log logger.Logger, 33 | cfg *config.Config, 34 | amqpConn *amqp.Connection, 35 | amqpChan *amqp.Channel, 36 | productUseCase domain.ProductUseCase, 37 | esClient *elasticsearch.Client, 38 | metrics *metrics.SearchMicroserviceMetrics, 39 | ) *consumer { 40 | return &consumer{log: log, cfg: cfg, amqpConn: amqpConn, amqpChan: amqpChan, productUseCase: productUseCase, esClient: esClient, metrics: metrics} 41 | } 42 | 43 | func (c *consumer) ConsumeIndexDeliveries(ctx context.Context, deliveries <-chan amqp.Delivery, workerID int) func() error { 44 | return func() error { 45 | c.log.Infof("starting consumer workerID: %d, for queue deliveries: %s", workerID, c.cfg.ExchangeAndQueueBindings.IndexProductBinding.QueueName) 46 | 47 | for { 48 | select { 49 | case <-ctx.Done(): 50 | c.log.Errorf("products consumer ctx done: %v", ctx.Err()) 51 | return ctx.Err() 52 | 53 | case msg, ok := <-deliveries: 54 | if !ok { 55 | c.log.Errorf("NOT OK deliveries channel closed for queue: %s", c.cfg.ExchangeAndQueueBindings.IndexProductBinding.QueueName) 56 | return errors.New("deliveries channel closed") 57 | } 58 | 59 | c.log.Infof("Consumer delivery: workerID: %d, msg data: %s, headers: %+v", workerID, string(msg.Body), msg.Headers) 60 | 61 | if err := c.bulkIndexProduct(ctx, msg); err != nil { 62 | c.log.Errorf("bulkIndexProduct err: %v", err) 63 | continue 64 | } 65 | 66 | c.log.Infof("Consumer <<>> delivery: workerID: %d, msg data: %s, headers: %+v", workerID, string(msg.Body), msg.Headers) 67 | c.metrics.RabbitMQSuccessBatchInsertMessages.Inc() 68 | } 69 | } 70 | 71 | } 72 | } 73 | 74 | func (c *consumer) indexProduct(ctx context.Context, msg amqp.Delivery) error { 75 | span, ctx := opentracing.StartSpanFromContext(ctx, "consumer.indexProduct") 76 | defer span.Finish() 77 | 78 | var product domain.Product 79 | if err := serializer.Unmarshal(msg.Body, &product); err != nil { 80 | c.log.Errorf("indexProduct serializer.Unmarshal <<>> err: %v", tracing.TraceWithErr(span, err)) 81 | return msg.Reject(true) 82 | } 83 | if err := c.productUseCase.Index(ctx, product); err != nil { 84 | c.log.Errorf("indexProduct productUseCase.Index <<>> err: %v", tracing.TraceWithErr(span, err)) 85 | return msg.Reject(true) 86 | } 87 | return msg.Ack(true) 88 | } 89 | 90 | func (c *consumer) bulkIndexProduct(ctx context.Context, msg amqp.Delivery) error { 91 | ctx, span := tracing.StartRabbitConsumerTracerSpan(ctx, msg.Headers, "consumer.bulkIndexProduct") 92 | defer span.Finish() 93 | 94 | var product domain.Product 95 | if err := serializer.Unmarshal(msg.Body, &product); err != nil { 96 | c.log.Errorf("indexProduct serializer.Unmarshal <<>> err: %v", tracing.TraceWithErr(span, err)) 97 | return msg.Reject(true) 98 | } 99 | 100 | if err := c.bulkIndexer.Add(ctx, esutil.BulkIndexerItem{ 101 | Index: c.cfg.ElasticIndexes.ProductsIndex.Name, 102 | Action: "index", 103 | DocumentID: product.ID, 104 | Body: bytes.NewReader(msg.Body), 105 | OnSuccess: func(ctx context.Context, item esutil.BulkIndexerItem, item2 esutil.BulkIndexerResponseItem) { 106 | c.log.Debugf("bulk indexer onSuccess for index alias: %s", c.cfg.ElasticIndexes.ProductsIndex.Alias) 107 | }, 108 | OnFailure: func(ctx context.Context, item esutil.BulkIndexerItem, item2 esutil.BulkIndexerResponseItem, err error) { 109 | if err != nil { 110 | c.log.Errorf("bulk indexer OnFailure err: %v", err) 111 | } 112 | }, 113 | }); err != nil { 114 | c.log.Errorf("indexProduct bulkIndexer.Add <<>> err: %v", tracing.TraceWithErr(span, err)) 115 | return msg.Reject(true) 116 | } 117 | 118 | c.log.Infof("consumer <<>> bulk indexer add product: %+v", product) 119 | return msg.Ack(true) 120 | } 121 | 122 | func (c *consumer) InitBulkIndexer() error { 123 | bulkIndexer, err := esutil.NewBulkIndexer(esutil.BulkIndexerConfig{ 124 | NumWorkers: c.cfg.BulkIndexerConfig.NumWorkers, 125 | FlushBytes: c.cfg.BulkIndexerConfig.FlushBytes, 126 | FlushInterval: time.Duration(c.cfg.BulkIndexerConfig.FlushIntervalSeconds) * time.Second, 127 | Client: c.esClient, 128 | OnError: func(ctx context.Context, err error) { 129 | c.log.Errorf("bulk indexer onError: %v", err) 130 | }, 131 | OnFlushStart: func(ctx context.Context) context.Context { 132 | c.log.Infof("starting bulk indexer for index alias: %s", c.cfg.ElasticIndexes.ProductsIndex.Alias) 133 | return ctx 134 | }, 135 | OnFlushEnd: func(ctx context.Context) { 136 | c.log.Infof("finished bulk indexer for index alias: %s", c.cfg.ElasticIndexes.ProductsIndex.Alias) 137 | }, 138 | Index: c.cfg.ElasticIndexes.ProductsIndex.Alias, 139 | Human: true, 140 | Pretty: true, 141 | Timeout: time.Duration(c.cfg.BulkIndexerConfig.TimeoutMilliseconds) * time.Second, 142 | }) 143 | if err != nil { 144 | return errors.Wrap(err, "esutil.NewBulkIndexer") 145 | } 146 | 147 | c.bulkIndexer = bulkIndexer 148 | c.log.Infof("consumer bulk indexer initialized for index alias: %s", c.cfg.ElasticIndexes.ProductsIndex.Alias) 149 | return nil 150 | } 151 | 152 | func (c *consumer) Close(ctx context.Context) { 153 | if err := c.bulkIndexer.Close(ctx); err != nil { 154 | c.log.Errorf("bulk indexer Close: %v", err) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /internal/product/delivery/rabbitmq/consumer_test.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/AleksK1NG/go-elasticsearch/internal/product/domain" 7 | "github.com/AleksK1NG/go-elasticsearch/pkg/logger" 8 | "github.com/AleksK1NG/go-elasticsearch/pkg/rabbitmq" 9 | "github.com/brianvoe/gofakeit/v6" 10 | amqp "github.com/rabbitmq/amqp091-go" 11 | uuid "github.com/satori/go.uuid" 12 | "github.com/stretchr/testify/require" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | func TestConsumer_ConsumeIndexDeliveries(t *testing.T) { 18 | t.Parallel() 19 | 20 | appLogger := logger.NewAppLogger(logger.LogConfig{ 21 | LogLevel: "debug", 22 | DevMode: false, 23 | Encoder: "console", 24 | }) 25 | appLogger.InitLogger() 26 | appLogger.Named("Publisher") 27 | 28 | amqpPublisher, err := rabbitmq.NewPublisher(rabbitmq.Config{URI: "amqp://guest:guest@localhost:5672/"}, appLogger) 29 | if err != nil { 30 | require.NoError(t, err) 31 | 32 | } 33 | defer amqpPublisher.Close() 34 | 35 | for i := 0; i < 1500; i++ { 36 | //time.Sleep(10 * time.Millisecond) 37 | 38 | product := domain.Product{ 39 | ID: uuid.NewV4().String(), 40 | Title: gofakeit.Fruit(), 41 | Description: gofakeit.AdjectiveDescriptive(), 42 | ImageURL: gofakeit.URL(), 43 | CountInStock: gofakeit.Int64(), 44 | Shop: gofakeit.Company(), 45 | CreatedAt: time.Now().UTC(), 46 | } 47 | 48 | dataBytes, err := json.Marshal(&product) 49 | if err != nil { 50 | require.NoError(t, err) 51 | } 52 | 53 | if err := amqpPublisher.Publish( 54 | context.Background(), 55 | "products", 56 | "products-index", 57 | amqp.Publishing{ 58 | Headers: map[string]interface{}{"trace-id": uuid.NewV4().String()}, 59 | Timestamp: time.Now().UTC(), 60 | Body: dataBytes, 61 | }, 62 | ); err != nil { 63 | require.NoError(t, err) 64 | } 65 | appLogger.Infof("message published: %+v", product) 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /internal/product/domain/model.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "time" 4 | 5 | type Product struct { 6 | ID string `json:"id"` 7 | Title string `json:"title"` 8 | Description string `json:"description"` 9 | ImageURL string `json:"image_url"` 10 | CountInStock int64 `json:"count_in_stock"` 11 | Shop string `json:"shop"` 12 | CreatedAt time.Time `json:"created_at"` 13 | } 14 | -------------------------------------------------------------------------------- /internal/product/domain/repository.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | "github.com/AleksK1NG/go-elasticsearch/pkg/utils" 6 | ) 7 | 8 | type ElasticRepository interface { 9 | Index(ctx context.Context, product Product) error 10 | Search(ctx context.Context, term string, pagination *utils.Pagination) (*ProductSearch, error) 11 | } 12 | -------------------------------------------------------------------------------- /internal/product/domain/search.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/AleksK1NG/go-elasticsearch/pkg/utils" 5 | ) 6 | 7 | type ProductSearch struct { 8 | List []*Product `json:"list"` 9 | PaginationResponse *utils.PaginationResponse `json:"paginationResponse"` 10 | } 11 | -------------------------------------------------------------------------------- /internal/product/domain/usecase.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | "github.com/AleksK1NG/go-elasticsearch/pkg/utils" 6 | ) 7 | 8 | type ProductUseCase interface { 9 | IndexAsync(ctx context.Context, product Product) error 10 | Index(ctx context.Context, product Product) error 11 | Search(ctx context.Context, term string, pagination *utils.Pagination) (*ProductSearch, error) 12 | } 13 | -------------------------------------------------------------------------------- /internal/product/repository/elastic_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "github.com/AleksK1NG/go-elasticsearch/config" 8 | "github.com/AleksK1NG/go-elasticsearch/internal/product/domain" 9 | "github.com/AleksK1NG/go-elasticsearch/pkg/esclient" 10 | "github.com/AleksK1NG/go-elasticsearch/pkg/keyboard_manager" 11 | "github.com/AleksK1NG/go-elasticsearch/pkg/logger" 12 | "github.com/AleksK1NG/go-elasticsearch/pkg/serializer" 13 | "github.com/AleksK1NG/go-elasticsearch/pkg/tracing" 14 | "github.com/AleksK1NG/go-elasticsearch/pkg/utils" 15 | "github.com/elastic/go-elasticsearch/v8" 16 | "github.com/opentracing/opentracing-go" 17 | "github.com/opentracing/opentracing-go/log" 18 | "github.com/pkg/errors" 19 | "time" 20 | ) 21 | 22 | const ( 23 | indexTimeout = 5 * time.Second 24 | searchTimeout = 5 * time.Second 25 | ) 26 | 27 | var ( 28 | searchFields = []string{"title", "description"} 29 | ) 30 | 31 | type esRepository struct { 32 | log logger.Logger 33 | cfg *config.Config 34 | esClient *elasticsearch.Client 35 | keyboardLayoutManager keyboard_manager.KeyboardLayoutManager 36 | } 37 | 38 | func NewEsRepository( 39 | log logger.Logger, 40 | cfg *config.Config, 41 | esClient *elasticsearch.Client, 42 | missTypeManager keyboard_manager.KeyboardLayoutManager, 43 | ) *esRepository { 44 | return &esRepository{log: log, cfg: cfg, esClient: esClient, keyboardLayoutManager: missTypeManager} 45 | } 46 | 47 | func (e *esRepository) Index(ctx context.Context, product domain.Product) error { 48 | span, ctx := opentracing.StartSpanFromContext(ctx, "esRepository.Index") 49 | defer span.Finish() 50 | span.LogFields(log.Object("product", product)) 51 | 52 | dataBytes, err := serializer.Marshal(&product) 53 | if err != nil { 54 | return tracing.TraceWithErr(span, errors.Wrap(err, "serializer.Marshal")) 55 | } 56 | 57 | response, err := e.esClient.Index( 58 | e.cfg.ElasticIndexes.ProductsIndex.Alias, 59 | bytes.NewReader(dataBytes), 60 | e.esClient.Index.WithPretty(), 61 | e.esClient.Index.WithHuman(), 62 | e.esClient.Index.WithTimeout(indexTimeout), 63 | e.esClient.Index.WithContext(ctx), 64 | e.esClient.Index.WithDocumentID(product.ID), 65 | ) 66 | if err != nil { 67 | return tracing.TraceWithErr(span, errors.Wrap(err, "esClient.Index")) 68 | } 69 | defer response.Body.Close() 70 | 71 | if response.IsError() { 72 | return tracing.TraceWithErr(span, errors.Wrap(errors.New(response.String()), "esClient.Index response error")) 73 | } 74 | 75 | e.log.Infof("document indexed: %s", response.String()) 76 | return nil 77 | } 78 | 79 | func (e *esRepository) Search(ctx context.Context, term string, pagination *utils.Pagination) (*domain.ProductSearch, error) { 80 | span, ctx := opentracing.StartSpanFromContext(ctx, "esRepository.Search") 81 | defer span.Finish() 82 | span.LogFields(log.String("term", term)) 83 | 84 | shouldQuery := map[string]any{ 85 | "query": map[string]any{ 86 | "bool": map[string]any{ 87 | "should": []map[string]any{ 88 | { 89 | "multi_match": map[string]any{ 90 | "query": term, 91 | "fields": searchFields, 92 | }, 93 | }, 94 | { 95 | "multi_match": map[string]any{ 96 | "query": e.keyboardLayoutManager.GetOppositeLayoutWord(term), 97 | "fields": searchFields, 98 | }, 99 | }, 100 | }, 101 | "must_not": []map[string]any{ 102 | { 103 | "range": map[string]any{ 104 | "count_in_stock": map[string]any{ 105 | "lt": 1, 106 | }, 107 | }, 108 | }, 109 | }, 110 | }, 111 | }, 112 | } 113 | 114 | dataBytes, err := serializer.Marshal(&shouldQuery) 115 | if err != nil { 116 | return nil, tracing.TraceWithErr(span, errors.Wrap(err, "serializer.Marshal")) 117 | } 118 | 119 | e.log.Debugf("Search json query: %+v", shouldQuery) 120 | 121 | response, err := e.esClient.Search( 122 | e.esClient.Search.WithContext(ctx), 123 | e.esClient.Search.WithIndex(e.cfg.ElasticIndexes.ProductsIndex.Alias), 124 | e.esClient.Search.WithBody(bufio.NewReader(bytes.NewReader(dataBytes))), 125 | e.esClient.Search.WithPretty(), 126 | e.esClient.Search.WithHuman(), 127 | e.esClient.Search.WithTimeout(searchTimeout), 128 | e.esClient.Search.WithSize(pagination.GetSize()), 129 | e.esClient.Search.WithFrom(pagination.GetOffset()), 130 | ) 131 | if err != nil { 132 | return nil, tracing.TraceWithErr(span, errors.Wrap(err, "esClient.Search")) 133 | } 134 | defer response.Body.Close() 135 | 136 | if response.IsError() { 137 | return nil, tracing.TraceWithErr(span, errors.Wrap(errors.New(response.String()), "esClient.Search error")) 138 | } 139 | 140 | e.log.Infof("repository search result: %s", response.String()) 141 | 142 | hits := esclient.EsHits[*domain.Product]{} 143 | if err := serializer.NewDecoder(response.Body).Decode(&hits); err != nil { 144 | return nil, tracing.TraceWithErr(span, errors.Wrap(err, "serializer.Decode")) 145 | } 146 | 147 | responseList := make([]*domain.Product, len(hits.Hits.Hits)) 148 | for i, source := range hits.Hits.Hits { 149 | responseList[i] = source.Source 150 | } 151 | 152 | e.log.Infof("repository search result responseList: %+v", responseList) 153 | return &domain.ProductSearch{ 154 | List: responseList, 155 | PaginationResponse: utils.NewPaginationResponse(hits.Hits.Total.Value, pagination), 156 | }, nil 157 | } 158 | -------------------------------------------------------------------------------- /internal/product/usecase/usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "github.com/AleksK1NG/go-elasticsearch/config" 6 | "github.com/AleksK1NG/go-elasticsearch/internal/product/domain" 7 | "github.com/AleksK1NG/go-elasticsearch/pkg/logger" 8 | "github.com/AleksK1NG/go-elasticsearch/pkg/rabbitmq" 9 | "github.com/AleksK1NG/go-elasticsearch/pkg/serializer" 10 | "github.com/AleksK1NG/go-elasticsearch/pkg/tracing" 11 | "github.com/AleksK1NG/go-elasticsearch/pkg/utils" 12 | "github.com/opentracing/opentracing-go" 13 | "github.com/opentracing/opentracing-go/log" 14 | "github.com/pkg/errors" 15 | amqp "github.com/rabbitmq/amqp091-go" 16 | "time" 17 | ) 18 | 19 | type productUseCase struct { 20 | log logger.Logger 21 | cfg *config.Config 22 | elasticRepository domain.ElasticRepository 23 | amqpPublisher rabbitmq.AmqpPublisher 24 | } 25 | 26 | func NewProductUseCase( 27 | log logger.Logger, 28 | cfg *config.Config, 29 | elasticRepository domain.ElasticRepository, 30 | amqpPublisher rabbitmq.AmqpPublisher, 31 | ) *productUseCase { 32 | return &productUseCase{log: log, cfg: cfg, elasticRepository: elasticRepository, amqpPublisher: amqpPublisher} 33 | } 34 | 35 | func (p *productUseCase) IndexAsync(ctx context.Context, product domain.Product) error { 36 | span, ctx := opentracing.StartSpanFromContext(ctx, "productUseCase.IndexAsync") 37 | defer span.Finish() 38 | span.LogFields(log.Object("product", product)) 39 | 40 | dataBytes, err := serializer.Marshal(&product) 41 | if err != nil { 42 | return tracing.TraceWithErr(span, errors.Wrap(err, "serializer.Marshal")) 43 | } 44 | 45 | return p.amqpPublisher.Publish( 46 | ctx, 47 | p.cfg.ExchangeAndQueueBindings.IndexProductBinding.ExchangeName, 48 | p.cfg.ExchangeAndQueueBindings.IndexProductBinding.BindingKey, 49 | amqp.Publishing{ 50 | Headers: tracing.ExtractTextMapCarrierHeadersToAmqpTable(span.Context()), 51 | Timestamp: time.Now().UTC(), 52 | Body: dataBytes, 53 | }, 54 | ) 55 | } 56 | 57 | func (p *productUseCase) Index(ctx context.Context, product domain.Product) error { 58 | span, ctx := opentracing.StartSpanFromContext(ctx, "productUseCase.Index") 59 | defer span.Finish() 60 | span.LogFields(log.Object("product", product)) 61 | return p.elasticRepository.Index(ctx, product) 62 | } 63 | 64 | func (p *productUseCase) Search(ctx context.Context, term string, pagination *utils.Pagination) (*domain.ProductSearch, error) { 65 | span, ctx := opentracing.StartSpanFromContext(ctx, "productUseCase.Search") 66 | defer span.Finish() 67 | span.LogFields(log.String("term", term)) 68 | return p.elasticRepository.Search(ctx, term, pagination) 69 | } 70 | -------------------------------------------------------------------------------- /k8s/microservice/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /k8s/microservice/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: microservice 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /k8s/microservice/templates/elastic.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: elastic 5 | labels: 6 | app: elastic 7 | spec: 8 | replicas: 1 9 | template: 10 | metadata: 11 | name: elastic 12 | labels: 13 | app: elastic 14 | spec: 15 | containers: 16 | - name: elastic 17 | image: elasticsearch:8.3.3 18 | imagePullPolicy: IfNotPresent 19 | resources: 20 | requests: 21 | memory: "512Mi" 22 | cpu: "300m" 23 | limits: 24 | memory: "1500Mi" 25 | cpu: "800m" 26 | env: 27 | - name: node.name 28 | value: node01 29 | - name: cluster.name 30 | value: es-cluster-8 31 | - name: discovery.type 32 | value: single-node 33 | - name: xpack.license.self_generated.type 34 | value: basic 35 | - name: xpack.security.enabled 36 | value: "false" 37 | - name: ES_JAVA_OPTS 38 | value: "-Xms512m -Xmx512m" 39 | restartPolicy: Always 40 | selector: 41 | matchLabels: 42 | app: elastic 43 | --- 44 | apiVersion: v1 45 | kind: Service 46 | metadata: 47 | name: elastic 48 | spec: 49 | selector: 50 | app: elastic 51 | ports: 52 | - port: 9200 53 | name: http 54 | - port: 9300 55 | name: http2 56 | type: NodePort -------------------------------------------------------------------------------- /k8s/microservice/templates/jaeger.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: jaeger 5 | labels: 6 | app: jaeger 7 | spec: 8 | replicas: 1 9 | template: 10 | metadata: 11 | name: jaeger 12 | labels: 13 | app: jaeger 14 | spec: 15 | containers: 16 | - name: jaeger 17 | image: jaegertracing/all-in-one:1.35 18 | imagePullPolicy: IfNotPresent 19 | env: 20 | - name: COLLECTOR_ZIPKIN_HTTP_PORT 21 | value: "9411" 22 | resources: 23 | requests: 24 | memory: "512Mi" 25 | cpu: "300m" 26 | limits: 27 | memory: "1000Mi" 28 | cpu: "500m" 29 | restartPolicy: Always 30 | selector: 31 | matchLabels: 32 | app: jaeger 33 | --- 34 | apiVersion: v1 35 | kind: Service 36 | metadata: 37 | name: jaeger 38 | spec: 39 | selector: 40 | app: jaeger 41 | ports: 42 | - port: 5775 43 | protocol: UDP 44 | name: "5775" 45 | - port: 6831 46 | protocol: UDP 47 | name: "6831" 48 | - port: 6832 49 | protocol: UDP 50 | name: "6832" 51 | - port: 5778 52 | protocol: TCP 53 | name: "5778" 54 | - port: 16686 55 | protocol: TCP 56 | name: "16686" 57 | - port: 14268 58 | protocol: TCP 59 | name: "14268" 60 | - port: 14250 61 | protocol: TCP 62 | name: "14250" 63 | - port: 9411 64 | protocol: TCP 65 | name: "9411" 66 | type: NodePort -------------------------------------------------------------------------------- /k8s/microservice/templates/kibana.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: kibana 5 | labels: 6 | app: kibana 7 | spec: 8 | replicas: 1 9 | template: 10 | metadata: 11 | name: kibana 12 | labels: 13 | app: kibana 14 | spec: 15 | containers: 16 | - name: kibana 17 | image: kibana:8.3.3 18 | imagePullPolicy: IfNotPresent 19 | resources: 20 | requests: 21 | memory: "512Mi" 22 | cpu: "300m" 23 | limits: 24 | memory: "1000Mi" 25 | cpu: "500m" 26 | env: 27 | - name: ELASTICSEARCH_HOSTS 28 | value: http://elastic:9200 29 | restartPolicy: Always 30 | selector: 31 | matchLabels: 32 | app: kibana 33 | --- 34 | apiVersion: v1 35 | kind: Service 36 | metadata: 37 | name: kibana 38 | spec: 39 | selector: 40 | app: kibana 41 | ports: 42 | - port: 5601 43 | name: http 44 | type: NodePort 45 | -------------------------------------------------------------------------------- /k8s/microservice/templates/rabbit.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: rabbitmq 5 | labels: 6 | app: rabbitmq 7 | spec: 8 | replicas: 1 9 | template: 10 | metadata: 11 | name: rabbitmq 12 | labels: 13 | app: rabbitmq 14 | spec: 15 | containers: 16 | - name: rabbitmq 17 | image: rabbitmq:3.9-management-alpine 18 | imagePullPolicy: IfNotPresent 19 | resources: 20 | requests: 21 | memory: "512Mi" 22 | cpu: "300m" 23 | limits: 24 | memory: "1000Mi" 25 | cpu: "500m" 26 | restartPolicy: Always 27 | selector: 28 | matchLabels: 29 | app: rabbitmq 30 | --- 31 | apiVersion: v1 32 | kind: Service 33 | metadata: 34 | name: rabbitmq 35 | spec: 36 | selector: 37 | app: rabbitmq 38 | ports: 39 | - port: 5672 40 | protocol: TCP 41 | name: rabbit 42 | - port: 15672 43 | protocol: TCP 44 | name: management 45 | type: NodePort -------------------------------------------------------------------------------- /k8s/microservice/templates/search-microservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ .Values.searchMicroserviceName }} 5 | labels: 6 | app: {{ .Values.searchMicroserviceName }} 7 | spec: 8 | replicas: {{ .Values.searchMicroserviceReplicas }} 9 | template: 10 | metadata: 11 | name: {{ .Values.searchMicroserviceName }} 12 | labels: 13 | app: {{ .Values.searchMicroserviceName }} 14 | spec: 15 | containers: 16 | - name: {{ .Values.searchMicroserviceName }} 17 | image: {{.Values.searchMicroserviceImage }} 18 | imagePullPolicy: Always 19 | resources: 20 | requests: 21 | memory: {{.Values.resources.requests.memory }} 22 | cpu: {{.Values.resources.requests.cpu }} 23 | limits: 24 | memory: {{.Values.resources.limits.memory }} 25 | cpu: {{.Values.resources.limits.cpu }} 26 | livenessProbe: 27 | httpGet: 28 | path: {{.Values.searchMicroserviceLivenessProbePath }} 29 | port: {{.Values.searchMicroserviceLivenessProbePort }} 30 | initialDelaySeconds: {{ .Values.searchMicroserviceInitialDelaySeconds }} 31 | periodSeconds: {{ .Values.searchMicroservicePeriodSeconds }} 32 | readinessProbe: 33 | httpGet: 34 | path: {{.Values.searchMicroserviceReadinessProbePath }} 35 | port: {{.Values.searchMicroserviceReadinessProbePort }} 36 | initialDelaySeconds: {{ .Values.searchMicroserviceInitialDelaySeconds }} 37 | periodSeconds: {{ .Values.searchMicroservicePeriodSeconds }} 38 | ports: 39 | - containerPort: {{.Values.searchMicroserviceHttpPort }} 40 | name: http 41 | - containerPort: {{.Values.searchMicroserviceMetricsPort }} 42 | name: metrics 43 | - containerPort: {{.Values.searchMicroserviceHealthcheckPort }} 44 | name: healthcheck 45 | env: 46 | - name: JAEGER_HOST_PORT 47 | value: {{ .Values.jaegerHotPost }} 48 | - name: ELASTIC_URL 49 | value: {{ .Values.elasticSearchURL }} 50 | - name: RABBITMQ_URI 51 | value: {{ .Values.rabbitMqURI }} 52 | - name: CONFIG_PATH 53 | value: "/search-config/search-config.yaml" 54 | volumeMounts: 55 | - name: config 56 | mountPath: "/search-config" 57 | restartPolicy: Always 58 | terminationGracePeriodSeconds: {{ .Values.searchMicroserviceTerminationGracePeriodSeconds }} 59 | volumes: 60 | - name: config 61 | configMap: 62 | name: {{ .Values.searchMicroserviceName }}-config-map 63 | items: 64 | - key: search-config.yaml 65 | path: search-config.yaml 66 | selector: 67 | matchLabels: 68 | app: {{ .Values.searchMicroserviceName }} 69 | 70 | --- 71 | 72 | apiVersion: v1 73 | kind: Service 74 | metadata: 75 | name: {{ .Values.searchMicroserviceName }}-service 76 | labels: 77 | app: {{ .Values.searchMicroserviceName }} 78 | spec: 79 | type: ClusterIP 80 | selector: 81 | app: {{ .Values.searchMicroserviceName }} 82 | ports: 83 | - name: http 84 | port: {{.Values.searchMicroserviceHttpPort }} 85 | protocol: TCP 86 | - name: healthcheck 87 | port: {{.Values.searchMicroserviceHealthcheckPort }} 88 | protocol: TCP 89 | targetPort: metrics 90 | - name: metrics 91 | port: {{.Values.searchMicroserviceMetricsPort }} 92 | protocol: TCP 93 | targetPort: metrics 94 | 95 | --- 96 | 97 | apiVersion: monitoring.coreos.com/v1 98 | kind: ServiceMonitor 99 | metadata: 100 | labels: 101 | release: monitoring 102 | name: {{ .Values.searchMicroserviceName }}-service-monitor 103 | namespace: default 104 | spec: 105 | selector: 106 | matchLabels: 107 | app: {{ .Values.searchMicroserviceName }} 108 | endpoints: 109 | - interval: 10s 110 | port: metrics 111 | path: {{.Values.prometheusPath }} 112 | namespaceSelector: 113 | matchNames: 114 | - default 115 | 116 | --- 117 | 118 | apiVersion: v1 119 | kind: ConfigMap 120 | metadata: 121 | name: {{ .Values.searchMicroserviceName }}-config-map 122 | data: 123 | search-config.yaml: | 124 | serviceName: search_microservice 125 | grpc: 126 | port: :5001 127 | development: true 128 | http: 129 | port: :{{ .Values.searchMicroserviceHttpPort }} 130 | development: {{ .Values.http.development }} 131 | basePath: {{ .Values.http.basePath }} 132 | productsPath: {{ .Values.http.productsPath }} 133 | debugErrorsResponse: {{ .Values.http.debugErrorsResponse }} 134 | ignoreLogUrls: {{ .Values.http.ignoreLogUrls }} 135 | probes: 136 | readinessPath: {{ .Values.searchMicroserviceReadinessProbePath }} 137 | livenessPath: {{ .Values.searchMicroserviceLivenessProbePath }} 138 | port: :{{ .Values.searchMicroserviceHealthcheckPort }} 139 | pprof: :6001 140 | prometheusPath: {{ .Values.prometheusPath }} 141 | prometheusPort: :{{.Values.searchMicroserviceMetricsPort }} 142 | checkIntervalSeconds: 10 143 | logger: 144 | level: {{ .Values.searchMicroserviceLogging.level }} 145 | devMode: {{ .Values.searchMicroserviceLogging.devMode }} 146 | encoder: {{ .Values.searchMicroserviceLogging.encoder }} 147 | jaeger: 148 | enable: true 149 | serviceName: {{ .Values.searchMicroserviceName }} 150 | hostPort: {{ .Values.jaegerHotPost }} 151 | logSpans: false 152 | timeouts: 153 | postgresInitMilliseconds: 1500 154 | postgresInitRetryCount: 3 155 | elasticSearch: 156 | addresses: [ {{ .Values.elasticSearchURL }} ] 157 | username: "" 158 | password: "" 159 | apiKey: "" 160 | enableLogging: false 161 | elasticIndexes: 162 | products: 163 | path: {{ .Values.elasticIndexes.products.path }} 164 | name: {{ .Values.elasticIndexes.products.name }} 165 | alias: {{ .Values.elasticIndexes.products.alias }} 166 | rabbitmq: 167 | uri: {{ .Values.rabbitMqURI }} 168 | exchangeAndQueueBindings: 169 | indexProductBinding: 170 | exchangeName: {{.Values.exchangeAndQueueBindings.indexProductBinding.exchangeName }} 171 | exchangeKind: {{.Values.exchangeAndQueueBindings.indexProductBinding.exchangeKind }} 172 | queueName: {{.Values.exchangeAndQueueBindings.indexProductBinding.queueName }} 173 | bindingKey: {{.Values.exchangeAndQueueBindings.indexProductBinding.bindingKey }} 174 | concurrency: {{.Values.exchangeAndQueueBindings.indexProductBinding.concurrency }} 175 | consumer: {{.Values.exchangeAndQueueBindings.indexProductBinding.consumer }} 176 | bulkIndexer: 177 | numWorkers: {{ .Values.bulkIndexer.numWorkers }} 178 | flushBytes: {{ .Values.bulkIndexer.flushBytes }} 179 | flushIntervalSeconds: {{ .Values.bulkIndexer.flushIntervalSeconds }} 180 | timeoutMilliseconds: {{ .Values.bulkIndexer.timeoutMilliseconds }} 181 | -------------------------------------------------------------------------------- /k8s/microservice/values.yaml: -------------------------------------------------------------------------------- 1 | searchMicroserviceName: search-microservice 2 | searchMicroserviceReplicas: 1 3 | searchMicroserviceImage: alexanderbryksin/search_microservice:latest 4 | searchMicroserviceInitialDelaySeconds: 15 5 | searchMicroserviceLivenessProbePath: /live 6 | searchMicroserviceLivenessProbePort: 3001 7 | searchMicroserviceReadinessProbePath: /ready 8 | searchMicroserviceReadinessProbePort: 3001 9 | searchMicroserviceHttpPort: 8000 10 | searchMicroserviceMetricsPort: 8001 11 | searchMicroserviceHealthcheckPort: 3001 12 | searchMicroservicePeriodSeconds: 5 13 | searchMicroserviceTerminationGracePeriodSeconds: 30 14 | 15 | searchMicroserviceLogging: 16 | level: info 17 | devMode: false 18 | encoder: json 19 | 20 | resources: 21 | requests: 22 | memory: "256Mi" 23 | cpu: "300m" 24 | limits: 25 | memory: "1000Mi" 26 | cpu: "1000m" 27 | 28 | jaegerHotPost: jaeger:6831 29 | rabbitMqURI: amqp://guest:guest@rabbitmq:5672/ 30 | elasticSearchURL: http://elastic:9200 31 | 32 | http: 33 | development: true 34 | basePath: /api/v1 35 | productsPath: /api/v1/products 36 | debugErrorsResponse: true 37 | ignoreLogUrls: [ "metrics", "swagger" ] 38 | 39 | prometheusPath: /metrics 40 | 41 | elasticIndexes: 42 | products: 43 | path: config/mappings.json 44 | name: products 45 | alias: products-alias 46 | 47 | exchangeAndQueueBindings: 48 | indexProductBinding: 49 | exchangeName: products 50 | exchangeKind: direct 51 | queueName: index-product 52 | bindingKey: products-index 53 | concurrency: 10 54 | consumer: products-consumer 55 | 56 | bulkIndexer: 57 | numWorkers: 10 58 | flushBytes: 20000000 59 | flushIntervalSeconds: 15 60 | timeoutMilliseconds: 5000 -------------------------------------------------------------------------------- /monitoring/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 10s 3 | evaluation_interval: 10s 4 | 5 | scrape_configs: 6 | - job_name: 'prometheus' 7 | static_configs: 8 | - targets: [ 'localhost:9090' ] 9 | 10 | - job_name: 'system' 11 | static_configs: 12 | - targets: [ 'host.docker.internal:9101' ] 13 | 14 | - job_name: 'search_microservice' 15 | static_configs: 16 | - targets: [ 'host.docker.internal:8001' ] -------------------------------------------------------------------------------- /pkg/constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | GrpcPort = "GRPC_PORT" 5 | HttpPort = "HTTP_PORT" 6 | ConfigPath = "CONFIG_PATH" 7 | KafkaBrokers = "KAFKA_BROKERS" 8 | JaegerHostPort = "JAEGER_HOST_PORT" 9 | RedisAddr = "REDIS_ADDR" 10 | MongoDbURI = "MONGO_URI" 11 | PostgresqlHost = "POSTGRES_HOST" 12 | PostgresqlPort = "POSTGRES_PORT" 13 | ElasticUrl = "ELASTIC_URL" 14 | TCP = "tcp" 15 | MIGRATIONS_DB_URL = "MIGRATIONS_DB_URL" 16 | RabbitMQ_URI = "RABBITMQ_URI" 17 | 18 | Yaml = "yaml" 19 | Redis = "redis" 20 | Kafka = "kafka" 21 | Postgres = "postgres" 22 | MongoDB = "mongo" 23 | ElasticSearch = "elasticSearch" 24 | RabbitMQ = "RabbitMQ" 25 | 26 | GRPC = "GRPC" 27 | SIZE = "SIZE" 28 | URI = "URI" 29 | STATUS = "STATUS" 30 | HTTP = "HTTP" 31 | ERROR = "ERROR" 32 | METHOD = "METHOD" 33 | METADATA = "METADATA" 34 | REQUEST = "REQUEST" 35 | REPLY = "REPLY" 36 | TIME = "TIME" 37 | 38 | Topic = "topic" 39 | Partition = "partition" 40 | Message = "message" 41 | WorkerID = "workerID" 42 | Headers = "headers" 43 | Offset = "offset" 44 | Time = "time" 45 | 46 | Page = "page" 47 | Size = "size" 48 | Search = "search" 49 | ID = "id" 50 | 51 | EsAll = "$all" 52 | 53 | Validate = "validate" 54 | FieldValidation = "field validation" 55 | RequiredHeaders = "required header" 56 | Base64 = "base64" 57 | Unmarshal = "unmarshal" 58 | Uuid = "uuid" 59 | Cookie = "cookie" 60 | Token = "token" 61 | Bcrypt = "bcrypt" 62 | SQLState = "sqlstate" 63 | 64 | MongoAggregateID = "aggregateID" 65 | 66 | MongoProjection = "(MongoDB Projection)" 67 | ElasticProjection = "(Elastic Projection)" 68 | 69 | EventType = "(EventType)" 70 | AggregateID = "(AggregateID)" 71 | Version = "(Version)" 72 | TimeStamp = "(TimeStamp)" 73 | Metadata = "(Metadata)" 74 | 75 | MessageSize = "MessageSize" 76 | 77 | BankAccountIndex = "aggregateID" 78 | BankAccountId = "BankAccountId" 79 | 80 | KafkaHeaders = "kafkaHeaders" 81 | 82 | Tcp = "tcp" 83 | ) 84 | -------------------------------------------------------------------------------- /pkg/elastic/elastic.go: -------------------------------------------------------------------------------- 1 | package elastic 2 | 3 | import ( 4 | "github.com/elastic/elastic-transport-go/v8/elastictransport" 5 | "github.com/elastic/go-elasticsearch/v8" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | type BulkIndexerConfig struct { 11 | NumWorkers int `mapstructure:"numWorkers" validate:"required"` 12 | FlushBytes int `mapstructure:"flushBytes" validate:"required"` 13 | FlushIntervalSeconds int `mapstructure:"flushIntervalSeconds" validate:"required"` 14 | TimeoutMilliseconds int `mapstructure:"timeoutMilliseconds" validate:"required"` 15 | } 16 | 17 | type Config struct { 18 | Addresses []string `mapstructure:"addresses" validate:"required"` 19 | Username string `mapstructure:"username"` 20 | Password string `mapstructure:"password"` 21 | 22 | APIKey string `mapstructure:"apiKey"` 23 | Header http.Header // Global HTTP request header. 24 | EnableLogging bool `mapstructure:"enableLogging"` 25 | } 26 | 27 | func NewElasticSearchClient(cfg Config) (*elasticsearch.Client, error) { 28 | 29 | config := elasticsearch.Config{ 30 | Addresses: cfg.Addresses, 31 | Username: cfg.Username, 32 | Password: cfg.Password, 33 | APIKey: cfg.APIKey, 34 | Header: cfg.Header, 35 | } 36 | 37 | if cfg.EnableLogging { 38 | config.Logger = &elastictransport.ColorLogger{Output: os.Stdout, EnableRequestBody: true, EnableResponseBody: true} 39 | } 40 | 41 | client, err := elasticsearch.NewClient(config) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return client, nil 47 | } 48 | -------------------------------------------------------------------------------- /pkg/esclient/esclient.go: -------------------------------------------------------------------------------- 1 | package esclient 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/elastic/go-elasticsearch/v8" 9 | "github.com/elastic/go-elasticsearch/v8/esapi" 10 | "github.com/pkg/errors" 11 | "time" 12 | ) 13 | 14 | type ElasticIndex struct { 15 | Path string `mapstructure:"path" validate:"required"` 16 | Name string `mapstructure:"name" validate:"required"` 17 | Alias string `mapstructure:"alias" validate:"required"` 18 | } 19 | 20 | func (e *ElasticIndex) String() string { 21 | return fmt.Sprintf("Name: %s, Alias: %s, Path: %s", e.Name, e.Alias, e.Path) 22 | } 23 | 24 | func Info(ctx context.Context, esClient *elasticsearch.Client) (*esapi.Response, error) { 25 | response, err := esClient.Info(esClient.Info.WithContext(ctx), esClient.Info.WithHuman()) 26 | if err != nil { 27 | return nil, err 28 | } 29 | if response.IsError() { 30 | return nil, errors.New(response.String()) 31 | } 32 | 33 | return response, nil 34 | } 35 | 36 | func CreateIndex(ctx context.Context, esClient *elasticsearch.Client, name string, data []byte) (*esapi.Response, error) { 37 | response, err := esClient.Indices.Create( 38 | name, 39 | esClient.Indices.Create.WithContext(ctx), 40 | esClient.Indices.Create.WithBody(bytes.NewReader(data)), 41 | esClient.Indices.Create.WithPretty(), 42 | esClient.Indices.Create.WithHuman(), 43 | esClient.Indices.Create.WithTimeout(3*time.Second), 44 | ) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | if response.IsError() { 50 | return nil, errors.New(response.String()) 51 | } 52 | 53 | return response, nil 54 | } 55 | 56 | func CreateAlias(ctx context.Context, esClient *elasticsearch.Client, indexes []string, name string, data []byte) (*esapi.Response, error) { 57 | response, err := esClient.Indices.PutAlias( 58 | indexes, 59 | name, 60 | esClient.Indices.PutAlias.WithBody(bytes.NewReader(data)), 61 | esClient.Indices.PutAlias.WithContext(ctx), 62 | esClient.Indices.PutAlias.WithHuman(), 63 | esClient.Indices.PutAlias.WithPretty(), 64 | esClient.Indices.PutAlias.WithTimeout(3*time.Second), 65 | ) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | if response.IsError() { 71 | return nil, errors.New(response.String()) 72 | } 73 | 74 | return response, nil 75 | } 76 | 77 | func Exists(ctx context.Context, esClient *elasticsearch.Client, indexes []string) (*esapi.Response, error) { 78 | response, err := esClient.Indices.Exists( 79 | indexes, 80 | esClient.Indices.Exists.WithContext(ctx), 81 | esClient.Indices.Exists.WithHuman(), 82 | esClient.Indices.Exists.WithPretty(), 83 | ) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | if response.IsError() && response.StatusCode != 404 { 89 | return nil, errors.New(response.String()) 90 | } 91 | 92 | return response, nil 93 | } 94 | 95 | func Search(ctx context.Context, esClient *elasticsearch.Client, index, term string, fields []string) (*esapi.Response, error) { 96 | //query := make(map[string]any, 5) 97 | //multiMatch := map[string]any{"multi_match": map[string]any{ 98 | // "query": term, 99 | // "fields": fields, 100 | //}} 101 | //query["bool"] = map[string]any{"must": []map[string]any{multiMatch}} 102 | // 103 | //multiMatchBytes, err := json.Marshal(&multiMatch) 104 | //if err != nil { 105 | // return nil, err 106 | //} 107 | // 108 | //log.Printf("json search: %s", string(multiMatchBytes)) 109 | //log.Printf("json search: %s", string(multiMatchBytes)) 110 | 111 | query := MultiMatchQuery{ 112 | Query: Query{ 113 | Bool: Bool{ 114 | Must: []any{MultiMatch{ 115 | Query: term, 116 | Fields: fields, 117 | }}, 118 | }, 119 | }, 120 | } 121 | 122 | dataBytes, err := json.Marshal(&query) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | response, err := esClient.Search( 128 | esClient.Search.WithContext(ctx), 129 | esClient.Search.WithIndex(index), 130 | esClient.Search.WithBody(bytes.NewReader(dataBytes)), 131 | esClient.Search.WithPretty(), 132 | esClient.Search.WithHuman(), 133 | esClient.Search.WithTimeout(5*time.Second), 134 | ) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | if response.IsError() { 140 | return nil, errors.Wrap(errors.New(response.String()), "esClient.Search error") 141 | } 142 | 143 | return response, nil 144 | } 145 | -------------------------------------------------------------------------------- /pkg/esclient/search_query.go: -------------------------------------------------------------------------------- 1 | package esclient 2 | 3 | type MultiMatchQuery struct { 4 | Query Query `json:"query"` 5 | } 6 | 7 | type Query struct { 8 | Bool Bool `json:"bool"` 9 | } 10 | 11 | type Bool struct { 12 | Must []any `json:"must"` 13 | } 14 | 15 | type MultiMatch struct { 16 | Query string `json:"query"` 17 | Fields []string `json:"fields"` 18 | } 19 | 20 | type EsHits[T any] struct { 21 | Hits struct { 22 | Total struct { 23 | Value int64 `json:"value"` 24 | } `json:"total"` 25 | Hits []struct { 26 | Source T `json:"_source"` 27 | } `json:"hits"` 28 | } `json:"hits"` 29 | } 30 | -------------------------------------------------------------------------------- /pkg/http_client/http_client.go: -------------------------------------------------------------------------------- 1 | package http_client 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/go-resty/resty/v2" 9 | ) 10 | 11 | const ( 12 | clientTimeout = 5 * time.Second 13 | dialContextTimeout = 5 * time.Second 14 | clientTLSHandshakeTimeout = 5 * time.Second 15 | clientRetryWaitTime = 300 * time.Millisecond 16 | retryCount = 3 17 | xaxIdleConns = 20 18 | maxConnsPerHost = 40 19 | idleConnTimeout = 120 * time.Second 20 | responseHeaderTimeout = 5 * time.Second 21 | ) 22 | 23 | func NewHttpClient(debugMode bool) *resty.Client { 24 | t := &http.Transport{ 25 | DialContext: (&net.Dialer{ 26 | Timeout: dialContextTimeout, 27 | }).DialContext, 28 | TLSHandshakeTimeout: clientTLSHandshakeTimeout, 29 | MaxIdleConns: xaxIdleConns, 30 | MaxConnsPerHost: maxConnsPerHost, 31 | IdleConnTimeout: idleConnTimeout, 32 | ResponseHeaderTimeout: responseHeaderTimeout, 33 | } 34 | 35 | client := resty.New(). 36 | SetDebug(debugMode). 37 | SetTimeout(clientTimeout). 38 | SetRetryCount(retryCount). 39 | SetRetryWaitTime(clientRetryWaitTime). 40 | SetTransport(t) 41 | 42 | return client 43 | } 44 | -------------------------------------------------------------------------------- /pkg/http_errors/http_errors.go: -------------------------------------------------------------------------------- 1 | package httpErrors 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/AleksK1NG/go-elasticsearch/pkg/constants" 9 | "github.com/pkg/errors" 10 | "net/http" 11 | "strings" 12 | "time" 13 | 14 | "github.com/go-playground/validator" 15 | "github.com/labstack/echo/v4" 16 | ) 17 | 18 | const ( 19 | ErrBadRequest = "Bad request" 20 | ErrNotFound = "Not Found" 21 | ErrUnauthorized = "Unauthorized" 22 | ErrRequestTimeout = "Request Timeout" 23 | ErrInvalidEmail = "Invalid email" 24 | ErrInvalidPassword = "Invalid password" 25 | ErrInvalidField = "Invalid field" 26 | ErrInternalServerError = "Internal Server Error" 27 | ) 28 | 29 | var ( 30 | BadRequest = errors.New("Bad request") 31 | WrongCredentials = errors.New("Wrong Credentials") 32 | NotFound = errors.New("Not Found") 33 | Unauthorized = errors.New("Unauthorized") 34 | Forbidden = errors.New("Forbidden") 35 | InternalServerError = errors.New("Internal Server Error") 36 | ) 37 | 38 | // RestErr Rest error interface 39 | type RestErr interface { 40 | Status() int 41 | Error() string 42 | Causes() interface{} 43 | ErrBody() RestError 44 | } 45 | 46 | // RestError Rest error struct 47 | type RestError struct { 48 | ErrStatus int `json:"status,omitempty"` 49 | ErrError string `json:"error,omitempty"` 50 | ErrMessage interface{} `json:"message,omitempty"` 51 | Timestamp time.Time `json:"timestamp,omitempty"` 52 | } 53 | 54 | // ErrBody Error body 55 | func (e RestError) ErrBody() RestError { 56 | return e 57 | } 58 | 59 | // Error Error() interface method 60 | func (e RestError) Error() string { 61 | return fmt.Sprintf("status: %d - errors: %s - causes: %v", e.ErrStatus, e.ErrError, e.ErrMessage) 62 | } 63 | 64 | // Status Error status 65 | func (e RestError) Status() int { 66 | return e.ErrStatus 67 | } 68 | 69 | // Causes RestError Causes 70 | func (e RestError) Causes() interface{} { 71 | return e.ErrMessage 72 | } 73 | 74 | // NewRestError New Rest Error 75 | func NewRestError(status int, err string, causes interface{}, debug bool) RestErr { 76 | restError := RestError{ 77 | ErrStatus: status, 78 | ErrError: err, 79 | Timestamp: time.Now().UTC(), 80 | } 81 | if debug { 82 | restError.ErrMessage = causes 83 | } 84 | return restError 85 | } 86 | 87 | // NewRestErrorWithMessage New Rest Error With Message 88 | func NewRestErrorWithMessage(status int, err string, causes interface{}) RestErr { 89 | return RestError{ 90 | ErrStatus: status, 91 | ErrError: err, 92 | ErrMessage: causes, 93 | Timestamp: time.Now().UTC(), 94 | } 95 | } 96 | 97 | // NewRestErrorFromBytes New Rest Error From Bytes 98 | func NewRestErrorFromBytes(bytes []byte) (RestErr, error) { 99 | var apiErr RestError 100 | if err := json.Unmarshal(bytes, &apiErr); err != nil { 101 | return nil, errors.New("invalid json") 102 | } 103 | return apiErr, nil 104 | } 105 | 106 | // NewBadRequestError New Bad Request Error 107 | func NewBadRequestError(ctx echo.Context, causes interface{}, debug bool) error { 108 | restError := RestError{ 109 | ErrStatus: http.StatusBadRequest, 110 | ErrError: BadRequest.Error(), 111 | Timestamp: time.Now().UTC(), 112 | } 113 | if debug { 114 | restError.ErrMessage = causes 115 | } 116 | return ctx.JSON(http.StatusBadRequest, restError) 117 | } 118 | 119 | // NewNotFoundError New Not Found Error 120 | func NewNotFoundError(ctx echo.Context, causes interface{}, debug bool) error { 121 | restError := RestError{ 122 | ErrStatus: http.StatusNotFound, 123 | ErrError: NotFound.Error(), 124 | Timestamp: time.Now().UTC(), 125 | } 126 | if debug { 127 | restError.ErrMessage = causes 128 | } 129 | return ctx.JSON(http.StatusNotFound, restError) 130 | } 131 | 132 | // NewUnauthorizedError New Unauthorized Error 133 | func NewUnauthorizedError(ctx echo.Context, causes interface{}, debug bool) error { 134 | 135 | restError := RestError{ 136 | ErrStatus: http.StatusUnauthorized, 137 | ErrError: Unauthorized.Error(), 138 | Timestamp: time.Now().UTC(), 139 | } 140 | if debug { 141 | restError.ErrMessage = causes 142 | } 143 | return ctx.JSON(http.StatusUnauthorized, restError) 144 | } 145 | 146 | // NewForbiddenError New Forbidden Error 147 | func NewForbiddenError(ctx echo.Context, causes interface{}, debug bool) error { 148 | 149 | restError := RestError{ 150 | ErrStatus: http.StatusForbidden, 151 | ErrError: Forbidden.Error(), 152 | Timestamp: time.Now().UTC(), 153 | } 154 | if debug { 155 | restError.ErrMessage = causes 156 | } 157 | return ctx.JSON(http.StatusForbidden, restError) 158 | } 159 | 160 | // NewInternalServerError New Internal Server Error 161 | func NewInternalServerError(ctx echo.Context, causes interface{}, debug bool) error { 162 | 163 | restError := RestError{ 164 | ErrStatus: http.StatusInternalServerError, 165 | ErrError: InternalServerError.Error(), 166 | Timestamp: time.Now().UTC(), 167 | } 168 | if debug { 169 | restError.ErrMessage = causes 170 | } 171 | return ctx.JSON(http.StatusInternalServerError, restError) 172 | } 173 | 174 | // ParseErrors Parser of error string messages returns RestError 175 | func ParseErrors(err error, debug bool) RestErr { 176 | switch { 177 | case errors.Is(err, sql.ErrNoRows): 178 | return NewRestError(http.StatusNotFound, ErrNotFound, err.Error(), debug) 179 | case errors.Is(err, context.DeadlineExceeded): 180 | return NewRestError(http.StatusRequestTimeout, ErrRequestTimeout, err.Error(), debug) 181 | case errors.Is(err, Unauthorized): 182 | return NewRestError(http.StatusUnauthorized, ErrUnauthorized, err.Error(), debug) 183 | case errors.Is(err, WrongCredentials): 184 | return NewRestError(http.StatusUnauthorized, ErrUnauthorized, err.Error(), debug) 185 | case strings.Contains(strings.ToLower(err.Error()), constants.SQLState): 186 | return parseSqlErrors(err, debug) 187 | case strings.Contains(strings.ToLower(err.Error()), "field validation"): 188 | if validationErrors, ok := err.(validator.ValidationErrors); ok { 189 | return NewRestError(http.StatusBadRequest, ErrBadRequest, validationErrors.Error(), debug) 190 | } 191 | return parseValidatorError(err, debug) 192 | case strings.Contains(strings.ToLower(err.Error()), "required header"): 193 | return NewRestError(http.StatusBadRequest, ErrBadRequest, err.Error(), debug) 194 | case strings.Contains(strings.ToLower(err.Error()), "no documents in result"): 195 | return NewRestError(http.StatusNotFound, ErrNotFound, err.Error(), debug) 196 | default: 197 | if restErr, ok := err.(*RestError); ok { 198 | return restErr 199 | } 200 | return NewRestError(http.StatusInternalServerError, ErrInternalServerError, errors.Cause(err).Error(), debug) 201 | } 202 | } 203 | 204 | func parseSqlErrors(err error, debug bool) RestErr { 205 | return NewRestError(http.StatusBadRequest, ErrBadRequest, err, debug) 206 | } 207 | 208 | func parseValidatorError(err error, debug bool) RestErr { 209 | if strings.Contains(err.Error(), "Password") { 210 | return NewRestError(http.StatusBadRequest, ErrInvalidPassword, err, debug) 211 | } 212 | 213 | if strings.Contains(err.Error(), "Email") { 214 | return NewRestError(http.StatusBadRequest, ErrInvalidEmail, err, debug) 215 | } 216 | 217 | return NewRestError(http.StatusBadRequest, ErrInvalidField, err, debug) 218 | } 219 | 220 | // ErrorResponse Error response 221 | func ErrorResponse(err error, debug bool) (int, interface{}) { 222 | return ParseErrors(err, debug).Status(), ParseErrors(err, debug) 223 | } 224 | 225 | // ErrorCtxResponse Error response object and status code 226 | func ErrorCtxResponse(ctx echo.Context, err error, debug bool) error { 227 | if err != nil { 228 | restErr := ParseErrors(err, debug) 229 | return ctx.JSON(restErr.Status(), restErr) 230 | } 231 | return ctx.JSON(http.StatusInternalServerError, ErrInternalServerError) 232 | } 233 | -------------------------------------------------------------------------------- /pkg/keyboard_manager/keyboard_manager.go: -------------------------------------------------------------------------------- 1 | package keyboard_manager 2 | 3 | import ( 4 | "github.com/AleksK1NG/go-elasticsearch/pkg/logger" 5 | "strings" 6 | "sync" 7 | ) 8 | 9 | type KeyboardLayoutManager interface { 10 | GetOppositeLayoutWord(originalWord string) string 11 | } 12 | 13 | type keyboardLayoutsManager struct { 14 | log logger.Logger 15 | keyMappings map[string]string 16 | sbPool *sync.Pool 17 | } 18 | 19 | func NewKeyboardLayoutManager(log logger.Logger, keyMappings map[string]string) *keyboardLayoutsManager { 20 | sbPool := &sync.Pool{New: func() any { return new(strings.Builder) }} 21 | return &keyboardLayoutsManager{log: log, keyMappings: keyMappings, sbPool: sbPool} 22 | } 23 | 24 | func (k *keyboardLayoutsManager) GetOppositeLayoutWord(originalWord string) string { 25 | originalWord = strings.ReplaceAll(originalWord, "Ё", "Е") 26 | originalWord = strings.ReplaceAll(originalWord, "ё", "е") 27 | 28 | sb := k.sbPool.Get().(*strings.Builder) 29 | defer k.sbPool.Put(sb) 30 | sb.Reset() 31 | 32 | for _, c := range originalWord { 33 | lowerCasedChar := strings.ToLower(string(c)) 34 | if char, ok := k.keyMappings[lowerCasedChar]; ok { 35 | sb.WriteString(char) 36 | } else { 37 | sb.WriteString(lowerCasedChar) 38 | } 39 | } 40 | return sb.String() 41 | } 42 | -------------------------------------------------------------------------------- /pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "github.com/AleksK1NG/go-elasticsearch/pkg/constants" 5 | "os" 6 | "time" 7 | 8 | "go.uber.org/zap" 9 | "go.uber.org/zap/zapcore" 10 | ) 11 | 12 | type LogConfig struct { 13 | LogLevel string `mapstructure:"level"` 14 | DevMode bool `mapstructure:"devMode"` 15 | Encoder string `mapstructure:"encoder"` 16 | } 17 | 18 | // Logger methods interface 19 | type Logger interface { 20 | InitLogger() 21 | Sync() error 22 | SetLogLevel(logLevel string) 23 | Debug(args ...interface{}) 24 | Debugf(template string, args ...interface{}) 25 | Info(args ...interface{}) 26 | Infof(template string, args ...interface{}) 27 | Warn(args ...interface{}) 28 | Warnf(template string, args ...interface{}) 29 | WarnErrMsg(msg string, err error) 30 | Error(args ...interface{}) 31 | Errorf(template string, args ...interface{}) 32 | Err(msg string, err error) 33 | DPanic(args ...interface{}) 34 | DPanicf(template string, args ...interface{}) 35 | Fatal(args ...interface{}) 36 | Fatalf(template string, args ...interface{}) 37 | Printf(template string, args ...interface{}) 38 | Named(name string) 39 | HttpMiddlewareAccessLogger(method string, uri string, status int, size int64, time time.Duration) 40 | GrpcMiddlewareAccessLogger(method string, time time.Duration, metaData map[string][]string, err error) 41 | GrpcMiddlewareAccessLoggerErr(method string, time time.Duration, metaData map[string][]string, err error) 42 | GrpcClientInterceptorLogger(method string, req interface{}, reply interface{}, time time.Duration, metaData map[string][]string, err error) 43 | GrpcClientInterceptorLoggerErr(method string, req, reply interface{}, time time.Duration, metaData map[string][]string, err error) 44 | KafkaProcessMessage(topic string, partition int, message []byte, workerID int, offset int64, time time.Time) 45 | KafkaLogCommittedMessage(topic string, partition int, offset int64) 46 | KafkaProcessMessageWithHeaders(topic string, partition int, message []byte, workerID int, offset int64, time time.Time, headers map[string]interface{}) 47 | } 48 | 49 | // Application logger 50 | type appLogger struct { 51 | level string 52 | devMode bool 53 | encoding string 54 | sugarLogger *zap.SugaredLogger 55 | logger *zap.Logger 56 | } 57 | 58 | // NewAppLogger App Logger constructor 59 | func NewAppLogger(cfg LogConfig) *appLogger { 60 | return &appLogger{level: cfg.LogLevel, devMode: cfg.DevMode, encoding: cfg.Encoder} 61 | } 62 | 63 | // For mapping config logger to email_service logger levels 64 | var loggerLevelMap = map[string]zapcore.Level{ 65 | "debug": zapcore.DebugLevel, 66 | "info": zapcore.InfoLevel, 67 | "warn": zapcore.WarnLevel, 68 | "error": zapcore.ErrorLevel, 69 | "dpanic": zapcore.DPanicLevel, 70 | "panic": zapcore.PanicLevel, 71 | "fatal": zapcore.FatalLevel, 72 | } 73 | 74 | func (l *appLogger) getLoggerLevel() zapcore.Level { 75 | level, exist := loggerLevelMap[l.level] 76 | if !exist { 77 | return zapcore.DebugLevel 78 | } 79 | 80 | return level 81 | } 82 | 83 | func (l *appLogger) setLoggerLevel(logLevel string) zapcore.Level { 84 | level, exist := loggerLevelMap[logLevel] 85 | if !exist { 86 | return zapcore.DebugLevel 87 | } 88 | return level 89 | } 90 | 91 | // InitLogger Init logger 92 | func (l *appLogger) InitLogger() { 93 | logLevel := l.getLoggerLevel() 94 | 95 | logWriter := zapcore.AddSync(os.Stdout) 96 | 97 | var encoderCfg zapcore.EncoderConfig 98 | if l.devMode { 99 | encoderCfg = zap.NewDevelopmentEncoderConfig() 100 | } else { 101 | encoderCfg = zap.NewProductionEncoderConfig() 102 | } 103 | 104 | var encoder zapcore.Encoder 105 | encoderCfg.NameKey = "[SERVICE]" 106 | encoderCfg.TimeKey = "[TIME]" 107 | encoderCfg.LevelKey = "[LEVEL]" 108 | encoderCfg.CallerKey = "[LINE]" 109 | encoderCfg.MessageKey = "[MESSAGE]" 110 | encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder 111 | encoderCfg.EncodeLevel = zapcore.CapitalLevelEncoder 112 | encoderCfg.EncodeCaller = zapcore.ShortCallerEncoder 113 | encoderCfg.EncodeDuration = zapcore.StringDurationEncoder 114 | 115 | if l.encoding == "console" { 116 | encoderCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder 117 | encoderCfg.EncodeCaller = zapcore.FullCallerEncoder 118 | encoderCfg.ConsoleSeparator = " | " 119 | encoder = zapcore.NewConsoleEncoder(encoderCfg) 120 | } else { 121 | encoderCfg.FunctionKey = "[CALLER]" 122 | encoderCfg.EncodeName = zapcore.FullNameEncoder 123 | encoder = zapcore.NewJSONEncoder(encoderCfg) 124 | } 125 | 126 | core := zapcore.NewCore(encoder, logWriter, zap.NewAtomicLevelAt(logLevel)) 127 | logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)) 128 | 129 | l.logger = logger 130 | l.sugarLogger = logger.Sugar() 131 | } 132 | 133 | func (l *appLogger) SetLogLevel(logLevel string) { 134 | logLvl := l.setLoggerLevel(logLevel) 135 | 136 | logWriter := zapcore.AddSync(os.Stdout) 137 | 138 | var encoderCfg zapcore.EncoderConfig 139 | if l.devMode { 140 | encoderCfg = zap.NewDevelopmentEncoderConfig() 141 | } else { 142 | encoderCfg = zap.NewProductionEncoderConfig() 143 | } 144 | 145 | var encoder zapcore.Encoder 146 | encoderCfg.NameKey = "[SERVICE]" 147 | encoderCfg.TimeKey = "[TIME]" 148 | encoderCfg.LevelKey = "[LEVEL]" 149 | encoderCfg.CallerKey = "[LINE]" 150 | encoderCfg.MessageKey = "[MESSAGE]" 151 | encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder 152 | encoderCfg.EncodeLevel = zapcore.CapitalLevelEncoder 153 | encoderCfg.EncodeCaller = zapcore.ShortCallerEncoder 154 | encoderCfg.EncodeDuration = zapcore.StringDurationEncoder 155 | 156 | if l.encoding == "console" { 157 | encoderCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder 158 | encoderCfg.EncodeCaller = zapcore.FullCallerEncoder 159 | encoderCfg.ConsoleSeparator = " | " 160 | encoder = zapcore.NewConsoleEncoder(encoderCfg) 161 | } else { 162 | encoderCfg.FunctionKey = "[CALLER]" 163 | encoderCfg.EncodeName = zapcore.FullNameEncoder 164 | encoder = zapcore.NewJSONEncoder(encoderCfg) 165 | } 166 | 167 | core := zapcore.NewCore(encoder, logWriter, zap.NewAtomicLevelAt(logLvl)) 168 | logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)) 169 | 170 | l.logger = logger 171 | l.sugarLogger = logger.Sugar() 172 | l.logger.Info("(SET LOG LEVEL)", zap.String("LEVEL", logLvl.CapitalString())) 173 | l.Sync() // nolint: errcheck 174 | } 175 | 176 | // Logger methods 177 | 178 | // Named add logger microservice name 179 | func (l *appLogger) Named(name string) { 180 | l.logger = l.logger.Named(name) 181 | l.sugarLogger = l.sugarLogger.Named(name) 182 | } 183 | 184 | // Debug uses fmt.Sprint to construct and log a message. 185 | func (l *appLogger) Debug(args ...interface{}) { 186 | l.sugarLogger.Debug(args...) 187 | } 188 | 189 | // Debugf uses fmt.Sprintf to log a templated message 190 | func (l *appLogger) Debugf(template string, args ...interface{}) { 191 | l.sugarLogger.Debugf(template, args...) 192 | } 193 | 194 | // Info uses fmt.Sprint to construct and log a message 195 | func (l *appLogger) Info(args ...interface{}) { 196 | l.sugarLogger.Info(args...) 197 | } 198 | 199 | // Infof uses fmt.Sprintf to log a templated message. 200 | func (l *appLogger) Infof(template string, args ...interface{}) { 201 | l.sugarLogger.Infof(template, args...) 202 | } 203 | 204 | // Printf uses fmt.Sprintf to log a templated message 205 | func (l *appLogger) Printf(template string, args ...interface{}) { 206 | l.sugarLogger.Infof(template, args...) 207 | } 208 | 209 | // Warn uses fmt.Sprint to construct and log a message. 210 | func (l *appLogger) Warn(args ...interface{}) { 211 | l.sugarLogger.Warn(args...) 212 | } 213 | 214 | // WarnErrMsg log error message with warn level. 215 | func (l *appLogger) WarnErrMsg(msg string, err error) { 216 | l.logger.Warn(msg, zap.String("error", err.Error())) 217 | } 218 | 219 | // Warnf uses fmt.Sprintf to log a templated message. 220 | func (l *appLogger) Warnf(template string, args ...interface{}) { 221 | l.sugarLogger.Warnf(template, args...) 222 | } 223 | 224 | // Error uses fmt.Sprint to construct and log a message. 225 | func (l *appLogger) Error(args ...interface{}) { 226 | l.sugarLogger.Error(args...) 227 | } 228 | 229 | // Errorf uses fmt.Sprintf to log a templated message. 230 | func (l *appLogger) Errorf(template string, args ...interface{}) { 231 | l.sugarLogger.Errorf(template, args...) 232 | } 233 | 234 | // Err uses error to log a message. 235 | func (l *appLogger) Err(msg string, err error) { 236 | l.logger.Error(msg, zap.Error(err)) 237 | } 238 | 239 | // DPanic uses fmt.Sprint to construct and log a message. In development, the logger then panics. (See DPanicLevel for details.) 240 | func (l *appLogger) DPanic(args ...interface{}) { 241 | l.sugarLogger.DPanic(args...) 242 | } 243 | 244 | // DPanicf uses fmt.Sprintf to log a templated message. In development, the logger then panics. (See DPanicLevel for details.) 245 | func (l *appLogger) DPanicf(template string, args ...interface{}) { 246 | l.sugarLogger.DPanicf(template, args...) 247 | } 248 | 249 | // Panic uses fmt.Sprint to construct and log a message, then panics. 250 | func (l *appLogger) Panic(args ...interface{}) { 251 | l.sugarLogger.Panic(args...) 252 | } 253 | 254 | // Panicf uses fmt.Sprintf to log a templated message, then panics 255 | func (l *appLogger) Panicf(template string, args ...interface{}) { 256 | l.sugarLogger.Panicf(template, args...) 257 | } 258 | 259 | // Fatal uses fmt.Sprint to construct and log a message, then calls os.Exit. 260 | func (l *appLogger) Fatal(args ...interface{}) { 261 | l.sugarLogger.Fatal(args...) 262 | } 263 | 264 | // Fatalf uses fmt.Sprintf to log a templated message, then calls os.Exit. 265 | func (l *appLogger) Fatalf(template string, args ...interface{}) { 266 | l.sugarLogger.Fatalf(template, args...) 267 | } 268 | 269 | // Sync flushes any buffered log entries 270 | func (l *appLogger) Sync() error { 271 | go l.logger.Sync() // nolint: errcheck 272 | return l.sugarLogger.Sync() 273 | } 274 | 275 | func (l *appLogger) HttpMiddlewareAccessLogger(method, uri string, status int, size int64, time time.Duration) { 276 | l.logger.Info( 277 | constants.HTTP, 278 | zap.String(constants.METHOD, method), 279 | zap.String(constants.URI, uri), 280 | zap.Int(constants.STATUS, status), 281 | zap.Int64(constants.SIZE, size), 282 | zap.Duration(constants.TIME, time), 283 | ) 284 | } 285 | 286 | func (l *appLogger) GrpcMiddlewareAccessLogger(method string, time time.Duration, metaData map[string][]string, err error) { 287 | l.logger.Info( 288 | constants.GRPC, 289 | zap.String(constants.METHOD, method), 290 | zap.Duration(constants.TIME, time), 291 | zap.Any(constants.METADATA, metaData), 292 | zap.Any(constants.ERROR, err), 293 | ) 294 | } 295 | 296 | func (l *appLogger) GrpcMiddlewareAccessLoggerErr(method string, time time.Duration, metaData map[string][]string, err error) { 297 | l.logger.Error( 298 | constants.GRPC, 299 | zap.String(constants.METHOD, method), 300 | zap.Duration(constants.TIME, time), 301 | zap.Any(constants.METADATA, metaData), 302 | zap.Any(constants.ERROR, err), 303 | ) 304 | } 305 | 306 | func (l *appLogger) GrpcClientInterceptorLogger(method string, req, reply interface{}, time time.Duration, metaData map[string][]string, err error) { 307 | l.logger.Info( 308 | constants.GRPC, 309 | zap.String(constants.METHOD, method), 310 | zap.Any(constants.REQUEST, req), 311 | zap.Any(constants.REPLY, reply), 312 | zap.Duration(constants.TIME, time), 313 | zap.Any(constants.METADATA, metaData), 314 | zap.Any(constants.ERROR, err), 315 | ) 316 | } 317 | 318 | func (l *appLogger) GrpcClientInterceptorLoggerErr(method string, req, reply interface{}, time time.Duration, metaData map[string][]string, err error) { 319 | l.logger.Error( 320 | constants.GRPC, 321 | zap.String(constants.METHOD, method), 322 | zap.Any(constants.REQUEST, req), 323 | zap.Any(constants.REPLY, reply), 324 | zap.Duration(constants.TIME, time), 325 | zap.Any(constants.METADATA, metaData), 326 | zap.Any(constants.ERROR, err), 327 | ) 328 | } 329 | 330 | func (l *appLogger) KafkaProcessMessage(topic string, partition int, message []byte, workerID int, offset int64, time time.Time) { 331 | l.logger.Debug( 332 | "(Processing Kafka message)", 333 | zap.String(constants.Topic, topic), 334 | zap.Int(constants.Partition, partition), 335 | zap.Int(constants.MessageSize, len(message)), 336 | zap.Int(constants.WorkerID, workerID), 337 | zap.Int64(constants.Offset, offset), 338 | zap.Time(constants.Time, time), 339 | ) 340 | } 341 | 342 | func (l *appLogger) KafkaLogCommittedMessage(topic string, partition int, offset int64) { 343 | l.logger.Debug( 344 | "(Committed Kafka message)", 345 | zap.String(constants.Topic, topic), 346 | zap.Int(constants.Partition, partition), 347 | zap.Int64(constants.Offset, offset), 348 | ) 349 | } 350 | 351 | func (l *appLogger) KafkaProcessMessageWithHeaders(topic string, partition int, message []byte, workerID int, offset int64, time time.Time, headers map[string]interface{}) { 352 | l.logger.Debug( 353 | "(Processing Kafka message)", 354 | zap.String(constants.Topic, topic), 355 | zap.Int(constants.Partition, partition), 356 | zap.Int(constants.MessageSize, len(message)), 357 | zap.Int(constants.WorkerID, workerID), 358 | zap.Int64(constants.Offset, offset), 359 | zap.Time(constants.Time, time), 360 | zap.Any(constants.KafkaHeaders, headers), 361 | ) 362 | } 363 | -------------------------------------------------------------------------------- /pkg/middlewares/manager.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "github.com/AleksK1NG/go-elasticsearch/config" 5 | "github.com/AleksK1NG/go-elasticsearch/pkg/logger" 6 | "github.com/labstack/echo/v4" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type MiddlewareMetricsCb func(err error) 12 | 13 | type MiddlewareManager interface { 14 | RequestLoggerMiddleware(next echo.HandlerFunc) echo.HandlerFunc 15 | } 16 | 17 | type middlewareManager struct { 18 | log logger.Logger 19 | cfg *config.Config 20 | metricsCb MiddlewareMetricsCb 21 | } 22 | 23 | func NewMiddlewareManager(log logger.Logger, cfg *config.Config, metricsCb MiddlewareMetricsCb) *middlewareManager { 24 | return &middlewareManager{log: log, cfg: cfg, metricsCb: metricsCb} 25 | } 26 | 27 | func (mw *middlewareManager) RequestLoggerMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 28 | return func(ctx echo.Context) error { 29 | 30 | start := time.Now() 31 | err := next(ctx) 32 | 33 | req := ctx.Request() 34 | res := ctx.Response() 35 | status := res.Status 36 | size := res.Size 37 | s := time.Since(start) 38 | 39 | if !mw.checkIgnoredURI(ctx.Request().RequestURI, mw.cfg.Http.IgnoreLogUrls) { 40 | mw.log.HttpMiddlewareAccessLogger(req.Method, req.URL.String(), status, size, s) 41 | } 42 | 43 | mw.metricsCb(err) 44 | return err 45 | } 46 | } 47 | 48 | func (mw *middlewareManager) checkIgnoredURI(requestURI string, uriList []string) bool { 49 | for _, s := range uriList { 50 | if strings.Contains(requestURI, s) { 51 | return true 52 | } 53 | } 54 | return false 55 | } 56 | -------------------------------------------------------------------------------- /pkg/probes/probes.go: -------------------------------------------------------------------------------- 1 | package probes 2 | 3 | type Config struct { 4 | ReadinessPath string `mapstructure:"readinessPath"` 5 | LivenessPath string `mapstructure:"livenessPath"` 6 | Port string `mapstructure:"port"` 7 | Pprof string `mapstructure:"pprof"` 8 | PrometheusPath string `mapstructure:"prometheusPath"` 9 | PrometheusPort string `mapstructure:"prometheusPort"` 10 | CheckIntervalSeconds int `mapstructure:"checkIntervalSeconds"` 11 | } 12 | -------------------------------------------------------------------------------- /pkg/rabbitmq/publisher.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "context" 5 | "github.com/AleksK1NG/go-elasticsearch/pkg/logger" 6 | amqp "github.com/rabbitmq/amqp091-go" 7 | ) 8 | 9 | type AmqpPublisher interface { 10 | PublishWithContext(ctx context.Context, exchange, key string, mandatory, immediate bool, msg amqp.Publishing) error 11 | Publish(ctx context.Context, exchange, key string, msg amqp.Publishing) error 12 | Close() 13 | } 14 | 15 | type publisher struct { 16 | log logger.Logger 17 | amqpConn *amqp.Connection 18 | amqpChan *amqp.Channel 19 | } 20 | 21 | func NewPublisher(cfg Config, log logger.Logger) (*publisher, error) { 22 | conn, err := NewRabbitMQConnection(cfg) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | channel, err := conn.Channel() 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return &publisher{ 33 | log: log, 34 | amqpConn: conn, 35 | amqpChan: channel, 36 | }, nil 37 | } 38 | 39 | func (p *publisher) Close() { 40 | if err := p.amqpChan.Close(); err != nil { 41 | p.log.Errorf("publisher mqpChan.Close err: %v", err) 42 | } 43 | if err := p.amqpConn.Close(); err != nil { 44 | p.log.Errorf("publisher amqpConn.Close err: %v", err) 45 | } 46 | } 47 | 48 | func (p *publisher) Publish(ctx context.Context, exchange, key string, msg amqp.Publishing) error { 49 | if err := p.amqpChan.PublishWithContext( 50 | ctx, 51 | exchange, 52 | key, 53 | false, 54 | false, 55 | msg, 56 | ); err != nil { 57 | p.log.Errorf("publisher Publish err: %v", err) 58 | return err 59 | } 60 | return nil 61 | } 62 | 63 | func (p *publisher) PublishWithContext(ctx context.Context, exchange, key string, mandatory, immediate bool, msg amqp.Publishing) error { 64 | if err := p.amqpChan.PublishWithContext( 65 | ctx, 66 | exchange, 67 | key, 68 | mandatory, 69 | immediate, 70 | msg, 71 | ); err != nil { 72 | p.log.Errorf("publisher PublishWithContext err: %v", err) 73 | return err 74 | } 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /pkg/rabbitmq/rabbitmq.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "context" 5 | amqp "github.com/rabbitmq/amqp091-go" 6 | "golang.org/x/sync/errgroup" 7 | ) 8 | 9 | type ExchangeConfig struct { 10 | Name string `mapstructure:"name" validate:"required"` 11 | Kind string `mapstructure:"kind" validate:"required"` 12 | } 13 | 14 | type QueueConfig struct { 15 | Name string `mapstructure:"name" validate:"required"` 16 | } 17 | 18 | type ExchangeAndQueueBinding struct { 19 | ExchangeName string `mapstructure:"exchangeName" validate:"required"` 20 | ExchangeKind string `mapstructure:"exchangeKind" validate:"required"` 21 | QueueName string `mapstructure:"queueName" validate:"required"` 22 | BindingKey string `mapstructure:"bindingKey" validate:"required"` 23 | Concurrency int `mapstructure:"concurrency" validate:"required"` 24 | Consumer string `mapstructure:"consumer" validate:"required"` 25 | } 26 | 27 | type Config struct { 28 | URI string `mapstructure:"uri" validate:"required"` 29 | } 30 | 31 | func NewRabbitMQConnection(cfg Config) (*amqp.Connection, error) { 32 | conn, err := amqp.Dial(cfg.URI) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | return conn, nil 38 | } 39 | 40 | func DeclareBinding(ctx context.Context, channel *amqp.Channel, exchangeAndQueueBinding ExchangeAndQueueBinding) (amqp.Queue, error) { 41 | if err := DeclareExchange(ctx, channel, exchangeAndQueueBinding.ExchangeName, exchangeAndQueueBinding.ExchangeKind); err != nil { 42 | return amqp.Queue{}, err 43 | } 44 | 45 | queue, err := DeclareQueue(ctx, channel, exchangeAndQueueBinding.QueueName) 46 | if err != nil { 47 | return amqp.Queue{}, err 48 | } 49 | 50 | if err := BindQueue(ctx, channel, queue.Name, exchangeAndQueueBinding.BindingKey, exchangeAndQueueBinding.ExchangeName); err != nil { 51 | return amqp.Queue{}, err 52 | } 53 | 54 | return queue, nil 55 | } 56 | 57 | func DeclareExchange(ctx context.Context, channel *amqp.Channel, name, kind string) error { 58 | return channel.ExchangeDeclare( 59 | name, 60 | kind, 61 | true, 62 | false, 63 | false, 64 | false, 65 | nil, 66 | ) 67 | } 68 | 69 | func DeclareQueue(ctx context.Context, channel *amqp.Channel, name string) (amqp.Queue, error) { 70 | return channel.QueueDeclare( 71 | name, 72 | true, 73 | false, 74 | false, 75 | false, 76 | nil, 77 | ) 78 | } 79 | 80 | func BindQueue(ctx context.Context, channel *amqp.Channel, queue, key, exchange string) error { 81 | return channel.QueueBind(queue, key, exchange, false, nil) 82 | } 83 | 84 | type ConsumeDeliveriesWorker interface { 85 | ConsumeDeliveries(ctx context.Context, deliveries <-chan amqp.Delivery) func() error 86 | } 87 | 88 | type DeliveriesConsumer func(ctx context.Context, deliveries <-chan amqp.Delivery, workerID int) func() error 89 | 90 | func ConsumeQueue(ctx context.Context, channel *amqp.Channel, concurrency int, queue string, consumer string, worker DeliveriesConsumer) error { 91 | deliveries, err := channel.Consume( 92 | queue, 93 | consumer, 94 | false, 95 | false, 96 | false, 97 | false, 98 | nil, 99 | ) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | eg, ctx := errgroup.WithContext(ctx) 105 | for i := 0; i <= concurrency; i++ { 106 | eg.Go(worker(ctx, deliveries, i)) 107 | } 108 | 109 | return eg.Wait() 110 | } 111 | -------------------------------------------------------------------------------- /pkg/serializer/serializer.go: -------------------------------------------------------------------------------- 1 | package serializer 2 | 3 | import ( 4 | jsoniter "github.com/json-iterator/go" 5 | "io" 6 | ) 7 | 8 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 9 | 10 | func Marshal(v any) ([]byte, error) { 11 | return json.Marshal(v) 12 | } 13 | 14 | func Unmarshal(data []byte, v any) error { 15 | return json.Unmarshal(data, v) 16 | } 17 | 18 | func NewDecoder(r io.Reader) *jsoniter.Decoder { 19 | return json.NewDecoder(r) 20 | } 21 | 22 | func NewEncoder(w io.Writer) *jsoniter.Encoder { 23 | return json.NewEncoder(w) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/tracing/jaeger.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "fmt" 5 | "github.com/opentracing/opentracing-go" 6 | "github.com/uber/jaeger-client-go" 7 | "github.com/uber/jaeger-client-go/config" 8 | "github.com/uber/jaeger-client-go/zipkin" 9 | "io" 10 | ) 11 | 12 | type Config struct { 13 | ServiceName string `mapstructure:"serviceName" validate:"required"` 14 | HostPort string `mapstructure:"hostPort" validate:"required"` 15 | Enable bool `mapstructure:"enable"` 16 | LogSpans bool `mapstructure:"logSpans"` 17 | } 18 | 19 | func (c *Config) String() string { 20 | return fmt.Sprintf("ServiceName: %s, HostPort: %s, Enable: %v, LogSpans: %v,", c.ServiceName, c.HostPort, c.Enable, c.LogSpans) 21 | } 22 | 23 | func NewJaegerTracer(jaegerConfig *Config) (opentracing.Tracer, io.Closer, error) { 24 | cfg := &config.Configuration{ 25 | ServiceName: jaegerConfig.ServiceName, 26 | 27 | // "const" sampler is a binary sampling strategy: 0=never sample, 1=always sample. 28 | Sampler: &config.SamplerConfig{ 29 | Type: "const", 30 | Param: 1, 31 | }, 32 | 33 | // Log the emitted spans to stdout. 34 | Reporter: &config.ReporterConfig{ 35 | LogSpans: jaegerConfig.LogSpans, 36 | LocalAgentHostPort: jaegerConfig.HostPort, 37 | }, 38 | } 39 | 40 | zipkinPropagator := zipkin.NewZipkinB3HTTPHeaderPropagator() 41 | 42 | return cfg.NewTracer( 43 | config.Logger(jaeger.StdLogger), 44 | config.Injector(opentracing.HTTPHeaders, zipkinPropagator), 45 | config.Injector(opentracing.TextMap, zipkinPropagator), 46 | config.Injector(opentracing.Binary, zipkinPropagator), 47 | config.Extractor(opentracing.HTTPHeaders, zipkinPropagator), 48 | config.Extractor(opentracing.TextMap, zipkinPropagator), 49 | config.Extractor(opentracing.Binary, zipkinPropagator), 50 | config.ZipkinSharedRPCSpan(false), 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/tracing/utils.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/labstack/echo/v4" 8 | "github.com/opentracing/opentracing-go" 9 | "github.com/opentracing/opentracing-go/ext" 10 | "github.com/rabbitmq/amqp091-go" 11 | "github.com/segmentio/kafka-go" 12 | "google.golang.org/grpc/metadata" 13 | ) 14 | 15 | func StartHttpServerTracerSpan(c echo.Context, operationName string) (context.Context, opentracing.Span) { 16 | spanCtx, err := opentracing.GlobalTracer().Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(c.Request().Header)) 17 | if err != nil { 18 | serverSpan := opentracing.GlobalTracer().StartSpan(operationName) 19 | ctx := opentracing.ContextWithSpan(c.Request().Context(), serverSpan) 20 | return ctx, serverSpan 21 | } 22 | 23 | serverSpan := opentracing.GlobalTracer().StartSpan(operationName, ext.RPCServerOption(spanCtx)) 24 | ctx := opentracing.ContextWithSpan(c.Request().Context(), serverSpan) 25 | 26 | return ctx, serverSpan 27 | } 28 | 29 | func GetTextMapCarrierFromMetaData(ctx context.Context) opentracing.TextMapCarrier { 30 | metadataMap := make(opentracing.TextMapCarrier) 31 | if md, ok := metadata.FromIncomingContext(ctx); ok { 32 | for key := range md.Copy() { 33 | metadataMap.Set(key, md.Get(key)[0]) 34 | } 35 | } 36 | return metadataMap 37 | } 38 | 39 | func StartGrpcServerTracerSpan(ctx context.Context, operationName string) (context.Context, opentracing.Span) { 40 | textMapCarrierFromMetaData := GetTextMapCarrierFromMetaData(ctx) 41 | 42 | span, err := opentracing.GlobalTracer().Extract(opentracing.TextMap, textMapCarrierFromMetaData) 43 | if err != nil { 44 | serverSpan := opentracing.GlobalTracer().StartSpan(operationName) 45 | ctx = opentracing.ContextWithSpan(ctx, serverSpan) 46 | return ctx, serverSpan 47 | } 48 | 49 | serverSpan := opentracing.GlobalTracer().StartSpan(operationName, ext.RPCServerOption(span)) 50 | ctx = opentracing.ContextWithSpan(ctx, serverSpan) 51 | 52 | return ctx, serverSpan 53 | } 54 | 55 | func StartKafkaConsumerTracerSpan(ctx context.Context, headers []kafka.Header, operationName string) (context.Context, opentracing.Span) { 56 | carrierFromKafkaHeaders := TextMapCarrierFromKafkaMessageHeaders(headers) 57 | 58 | spanCtx, err := opentracing.GlobalTracer().Extract(opentracing.TextMap, carrierFromKafkaHeaders) 59 | if err != nil { 60 | serverSpan := opentracing.GlobalTracer().StartSpan(operationName) 61 | ctx = opentracing.ContextWithSpan(ctx, serverSpan) 62 | return ctx, serverSpan 63 | } 64 | 65 | serverSpan := opentracing.GlobalTracer().StartSpan(operationName, ext.RPCServerOption(spanCtx)) 66 | ctx = opentracing.ContextWithSpan(ctx, serverSpan) 67 | 68 | return ctx, serverSpan 69 | } 70 | 71 | func StartRabbitConsumerTracerSpan(ctx context.Context, headers amqp091.Table, operationName string) (context.Context, opentracing.Span) { 72 | carrierFromRabbitHeaders := TextMapCarrierFromRabbitMessageHeaders(headers) 73 | 74 | spanCtx, err := opentracing.GlobalTracer().Extract(opentracing.TextMap, carrierFromRabbitHeaders) 75 | if err != nil { 76 | serverSpan := opentracing.GlobalTracer().StartSpan(operationName) 77 | ctx = opentracing.ContextWithSpan(ctx, serverSpan) 78 | return ctx, serverSpan 79 | } 80 | 81 | serverSpan := opentracing.GlobalTracer().StartSpan(operationName, ext.RPCServerOption(spanCtx)) 82 | ctx = opentracing.ContextWithSpan(ctx, serverSpan) 83 | 84 | return ctx, serverSpan 85 | } 86 | 87 | func TextMapCarrierToKafkaMessageHeaders(textMap opentracing.TextMapCarrier) []kafka.Header { 88 | headers := make([]kafka.Header, 0, len(textMap)) 89 | 90 | if err := textMap.ForeachKey(func(key, val string) error { 91 | headers = append(headers, kafka.Header{ 92 | Key: key, 93 | Value: []byte(val), 94 | }) 95 | return nil 96 | }); err != nil { 97 | return headers 98 | } 99 | 100 | return headers 101 | } 102 | 103 | func TextMapCarrierFromKafkaMessageHeaders(headers []kafka.Header) opentracing.TextMapCarrier { 104 | textMap := make(map[string]string, len(headers)) 105 | for _, header := range headers { 106 | textMap[header.Key] = string(header.Value) 107 | } 108 | return opentracing.TextMapCarrier(textMap) 109 | } 110 | 111 | func TextMapCarrierFromRabbitMessageHeaders(headers map[string]any) opentracing.TextMapCarrier { 112 | textMap := make(map[string]string, len(headers)) 113 | for key, value := range headers { 114 | textMap[key] = fmt.Sprintf("%v", value) 115 | } 116 | return opentracing.TextMapCarrier(textMap) 117 | } 118 | 119 | func InjectTextMapCarrier(spanCtx opentracing.SpanContext) (opentracing.TextMapCarrier, error) { 120 | m := make(opentracing.TextMapCarrier) 121 | if err := opentracing.GlobalTracer().Inject(spanCtx, opentracing.TextMap, m); err != nil { 122 | return nil, err 123 | } 124 | return m, nil 125 | } 126 | 127 | func InjectTextMapCarrierToGrpcMetaData(ctx context.Context, spanCtx opentracing.SpanContext) context.Context { 128 | if textMapCarrier, err := InjectTextMapCarrier(spanCtx); err == nil { 129 | md := metadata.New(textMapCarrier) 130 | ctx = metadata.NewOutgoingContext(ctx, md) 131 | } 132 | return ctx 133 | } 134 | 135 | func GetKafkaTracingHeadersFromSpanCtx(spanCtx opentracing.SpanContext) []kafka.Header { 136 | textMapCarrier, err := InjectTextMapCarrier(spanCtx) 137 | if err != nil { 138 | return []kafka.Header{} 139 | } 140 | 141 | kafkaMessageHeaders := TextMapCarrierToKafkaMessageHeaders(textMapCarrier) 142 | return kafkaMessageHeaders 143 | } 144 | 145 | func ExtractTextMapCarrier(spanCtx opentracing.SpanContext) opentracing.TextMapCarrier { 146 | textMapCarrier, err := InjectTextMapCarrier(spanCtx) 147 | if err != nil { 148 | return make(opentracing.TextMapCarrier) 149 | } 150 | return textMapCarrier 151 | } 152 | 153 | func ExtractTextMapCarrierHeaders(spanCtx opentracing.SpanContext) map[string]string { 154 | textMapCarrier, err := InjectTextMapCarrier(spanCtx) 155 | if err != nil { 156 | return make(opentracing.TextMapCarrier) 157 | } 158 | return textMapCarrier 159 | } 160 | 161 | func ExtractTextMapCarrierHeadersToAmqpTable(spanCtx opentracing.SpanContext) amqp091.Table { 162 | textMapCarrier, err := InjectTextMapCarrier(spanCtx) 163 | if err != nil { 164 | return make(amqp091.Table) 165 | } 166 | 167 | amqpTable := make(amqp091.Table, len(textMapCarrier)) 168 | for key, value := range textMapCarrier { 169 | amqpTable[key] = value 170 | } 171 | 172 | return amqpTable 173 | } 174 | 175 | func ExtractTextMapCarrierBytes(spanCtx opentracing.SpanContext) []byte { 176 | textMapCarrier, err := InjectTextMapCarrier(spanCtx) 177 | if err != nil { 178 | return []byte("") 179 | } 180 | 181 | dataBytes, err := json.Marshal(&textMapCarrier) 182 | if err != nil { 183 | return []byte("") 184 | } 185 | return dataBytes 186 | } 187 | 188 | func TraceErr(span opentracing.Span, err error) { 189 | span.SetTag("error", true) 190 | span.LogKV("error_code", err.Error()) 191 | } 192 | 193 | func TraceWithErr(span opentracing.Span, err error) error { 194 | if err != nil { 195 | span.SetTag("error", true) 196 | span.LogKV("error_code", err.Error()) 197 | } 198 | 199 | return err 200 | } 201 | -------------------------------------------------------------------------------- /pkg/utils/pagination.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | ) 8 | 9 | const ( 10 | defaultSize = 10 11 | defaultPage = 1 12 | ) 13 | 14 | type PaginationResponse struct { 15 | TotalCount int64 `json:"totalCount"` 16 | TotalPages int64 `json:"totalPages"` 17 | Page int64 `json:"page"` 18 | Size int64 `json:"size"` 19 | HasMore bool `json:"hasMore"` 20 | } 21 | 22 | func (p *PaginationResponse) String() string { 23 | return fmt.Sprintf("TotalCount: %d, TotalPages: %d, Page: %d, Size: %d, HasMore: %v", p.TotalCount, p.TotalPages, p.Page, p.Size, p.HasMore) 24 | } 25 | 26 | func NewPaginationResponse(totalCount int64, pq *Pagination) *PaginationResponse { 27 | return &PaginationResponse{ 28 | TotalCount: totalCount, 29 | TotalPages: int64(pq.GetTotalPages(int(totalCount))), 30 | Page: int64(pq.GetPage()), 31 | Size: int64(pq.GetSize()), 32 | HasMore: pq.GetHasMore(int(totalCount)), 33 | } 34 | } 35 | 36 | // Pagination query params 37 | type Pagination struct { 38 | Size int `json:"size,omitempty"` 39 | Page int `json:"page,omitempty"` 40 | OrderBy string `json:"orderBy,omitempty"` 41 | } 42 | 43 | // NewPaginationQuery Pagination query constructor 44 | func NewPaginationQuery(size int, page int) *Pagination { 45 | if size == 0 { 46 | return &Pagination{Size: defaultSize, Page: defaultPage} 47 | } 48 | return &Pagination{Size: size, Page: page} 49 | } 50 | 51 | func NewPaginationFromQueryParams(size string, page string) *Pagination { 52 | p := &Pagination{Size: defaultSize, Page: 1} 53 | 54 | if sizeNum, err := strconv.Atoi(size); err == nil && sizeNum != 0 { 55 | p.Size = sizeNum 56 | } 57 | 58 | if pageNum, err := strconv.Atoi(page); err == nil && pageNum != 0 { 59 | p.Page = pageNum 60 | } 61 | 62 | return p 63 | } 64 | 65 | // SetSize Set page size 66 | func (q *Pagination) SetSize(sizeQuery string) error { 67 | if sizeQuery == "" { 68 | q.Size = defaultSize 69 | return nil 70 | } 71 | n, err := strconv.Atoi(sizeQuery) 72 | if err != nil { 73 | return err 74 | } 75 | q.Size = n 76 | 77 | return nil 78 | } 79 | 80 | // SetPage Set page number 81 | func (q *Pagination) SetPage(pageQuery string) error { 82 | if pageQuery == "" { 83 | q.Size = 0 84 | return nil 85 | } 86 | n, err := strconv.Atoi(pageQuery) 87 | if err != nil { 88 | return err 89 | } 90 | q.Page = n 91 | 92 | return nil 93 | } 94 | 95 | // SetOrderBy Set order by 96 | func (q *Pagination) SetOrderBy(orderByQuery string) { 97 | q.OrderBy = orderByQuery 98 | } 99 | 100 | // GetOffset Get offset 101 | func (q *Pagination) GetOffset() int { 102 | if q.Page == 0 { 103 | return 0 104 | } 105 | return (q.Page - 1) * q.Size 106 | } 107 | 108 | // GetLimit Get limit 109 | func (q *Pagination) GetLimit() int { 110 | return q.Size 111 | } 112 | 113 | // GetOrderBy Get OrderBy 114 | func (q *Pagination) GetOrderBy() string { 115 | return q.OrderBy 116 | } 117 | 118 | // GetPage Get OrderBy 119 | func (q *Pagination) GetPage() int { 120 | return q.Page 121 | } 122 | 123 | // GetSize Get OrderBy 124 | func (q *Pagination) GetSize() int { 125 | return q.Size 126 | } 127 | 128 | // GetQueryString get query string 129 | func (q *Pagination) GetQueryString() string { 130 | return fmt.Sprintf("page=%d&size=%d&orderBy=%s", q.GetPage(), q.GetSize(), q.GetOrderBy()) 131 | } 132 | 133 | // GetTotalPages Get total pages int 134 | func (q *Pagination) GetTotalPages(totalCount int) int { 135 | d := float64(totalCount) / float64(q.GetSize()) 136 | return int(math.Ceil(d)) 137 | } 138 | 139 | // GetHasMore Get has more 140 | func (q *Pagination) GetHasMore(totalCount int) bool { 141 | return q.GetPage() < totalCount/q.GetSize() 142 | } 143 | --------------------------------------------------------------------------------