├── .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 |
--------------------------------------------------------------------------------