├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── build-container ├── build ├── build.sh └── test.sh ├── circle.yml ├── cmd └── event-srv │ ├── main.go │ └── serve.go ├── docker-compose.yml ├── glide.lock ├── glide.yaml └── pkg ├── checker ├── api.go ├── checker.go ├── elastic.go └── kafka.go ├── config └── config.go ├── datastore ├── datastore.go ├── elastic.go └── error.go ├── handlers ├── events.go ├── events_test.go ├── healthz.go └── healthz_test.go ├── kafka-bus ├── config.go ├── emitter.go ├── listener.go └── message.go ├── middleware └── logger_request.go ├── mocks ├── checker_mock.go ├── emitter_mock.go └── event_repo_mock.go ├── models ├── event.go └── query.go ├── render └── render.go └── repos └── event.go /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /Godeps 3 | /dist 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | MAINTAINER Rafael Jesus 3 | 4 | ADD dist/event-srv /dist/event-srv 5 | 6 | ENV EVENT_SRV_PORT="3000" 7 | ENV EVENT_SRV_DB="http://@docker:9200" 8 | ENV EVENT_SRV_BUS="localhost:9093" 9 | 10 | ENTRYPOINT ["/event-srv"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rafael Jesus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NO_COLOR=\033[0m 2 | OK_COLOR=\033[32;01m 3 | ERROR_COLOR=\033[31;01m 4 | WARN_COLOR=\033[33;01m 5 | 6 | IGNORED_PACKAGES := /vendor/ 7 | 8 | .PHONY: all clean deps build 9 | 10 | all: clean deps test build 11 | 12 | deps: 13 | @echo "$(OK_COLOR)==> Installing glide dependencies$(NO_COLOR)" 14 | @go get -u github.com/Masterminds/glide 15 | @glide install 16 | 17 | build: 18 | @echo "$(OK_COLOR)==> Building... $(NO_COLOR)" 19 | /bin/sh -c "VERSION=${VERSION} ./build/build.sh" 20 | 21 | test: 22 | @/bin/sh -c "./build/test.sh $(allpackages)" 23 | 24 | clean: 25 | @echo "$(OK_COLOR)==> Cleaning project$(NO_COLOR)" 26 | @go clean 27 | @rm -rf dist 28 | 29 | _allpackages = $(shell ( go list ./... 2>&1 1>&3 | \ 30 | grep -v -e "^$$" $(addprefix -e ,$(IGNORED_PACKAGES)) 1>&2 ) 3>&1 | \ 31 | grep -v -e "^$$" $(addprefix -e ,$(IGNORED_PACKAGES))) 32 | 33 | allpackages = $(if $(__allpackages),,$(eval __allpackages := $$(_allpackages)))$(__allpackages) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Event Service 2 | 3 | * An event log as a service 4 | * Build with Go, Elastic Search and Kafka 5 | * A minimal docker container 6 | * Automatically pushes it to dockerhub if tests pass 7 | 8 | ## Setup 9 | Env vars 10 | ```bash 11 | export PORT=3000 12 | export DATASTORE_URL=http://@docker:9200 13 | export BROKER_URL=localhost:9093 14 | ``` 15 | 16 | Installation 17 | ```sh 18 | mkdir -p $GOPATH/src/github.com/rafaeljesus 19 | cd $GOPATH/src/github.com/rafaeljesus 20 | git clone https://github.com/rafaeljesus/event-srv.git 21 | cd event-srv 22 | make all 23 | ``` 24 | 25 | ## Running server 26 | ``` 27 | ./dist/event-srv 28 | # => Starting Event Service at port 3000 29 | ``` 30 | 31 | ### Create a Event through HTTP 32 | - Request 33 | ```bash 34 | curl -X POST -H "Content-Type: application/json" \ 35 | -d '{"name": "order_created", "status": "success", "payload": {}}' \ 36 | localhost:3000/events 37 | ``` 38 | 39 | - Response 40 | ``` 41 | "OK" 42 | ``` 43 | 44 | ## Contributing 45 | - Fork it 46 | - Create your feature branch (`git checkout -b my-new-feature`) 47 | - Commit your changes (`git commit -am 'Add some feature'`) 48 | - Push to the branch (`git push origin my-new-feature`) 49 | - Create new Pull Request 50 | 51 | ## Badges 52 | 53 | [![CircleCI](https://circleci.com/gh/rafaeljesus/event-srv.svg?style=svg)](https://circleci.com/gh/rafaeljesus/event-srv) 54 | [![](https://images.microbadger.com/badges/image/rafaeljesus/event-srv.svg)](https://microbadger.com/images/rafaeljesus/event-srv "Get your own image badge on microbadger.com") 55 | [![](https://images.microbadger.com/badges/version/rafaeljesus/event-srv.svg)](https://microbadger.com/images/rafaeljesus/event-srv "Get your own version badge on microbadger.com") 56 | 57 | --- 58 | 59 | > GitHub [@rafaeljesus](https://github.com/rafaeljesus)  ·  60 | > Twitter [@rafaeljesus](https://twitter.com/_jesus_rafael) 61 | -------------------------------------------------------------------------------- /build-container: -------------------------------------------------------------------------------- 1 | bash make build 2 | docker build --no-cache --rm -t rafaeljesus/event-srv . 3 | -------------------------------------------------------------------------------- /build/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | rm -f dist/event-srv* 6 | 7 | if [ -z "$VERSION" ]; then 8 | VERSION="0.0.1-dev" 9 | fi 10 | echo "Building application version $VERSION" 11 | 12 | echo "Building default binaries" 13 | CGO_ENABLED=0 go build -ldflags "-s -w" -ldflags "-X main.version=${VERSION}" -o "dist/event-srv" github.com/rafaeljesus/event-srv/cmd/event-srv 14 | 15 | OS_PLATFORM_ARG=(linux darwin) 16 | OS_ARCH_ARG=(amd64) 17 | for OS in ${OS_PLATFORM_ARG[@]}; do 18 | for ARCH in ${OS_ARCH_ARG[@]}; do 19 | echo "Building binaries for $OS/$ARCH..." 20 | GOARCH=$ARCH GOOS=$OS CGO_ENABLED=0 go build -ldflags "-s -w" -ldflags "-X main.version=${VERSION}" -o "dist/event-srv_$OS-$ARCH" github.com/rafaeljesus/event-srv/cmd/event-srv 21 | done 22 | done 23 | -------------------------------------------------------------------------------- /build/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright 2016 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -e 18 | 19 | export CGO_ENABLED=1 20 | NO_COLOR='\033[0m' 21 | OK_COLOR='\033[32;01m' 22 | ERROR_COLOR='\033[31;01m' 23 | WARN_COLOR='\033[33;01m' 24 | PASS="${OK_COLOR}PASS ${NO_COLOR}" 25 | FAIL="${ERROR_COLOR}FAIL ${NO_COLOR}" 26 | 27 | TARGETS=$@ 28 | 29 | echo "${OK_COLOR}Running tests: ${NO_COLOR}" 30 | go test -race ${TARGETS} 31 | 32 | echo "${OK_COLOR}Formatting: ${NO_COLOR}" 33 | ERRS=$(find cmd pkg -type f -name \*.go | xargs gofmt -l 2>&1 || true) 34 | if [ -n "${ERRS}" ]; then 35 | echo "${ERROR_COLOR}FAIL - the following files need to be gofmt'ed: ${NO_COLOR}" 36 | for e in ${ERRS}; do 37 | echo " $e" 38 | done 39 | exit 1 40 | fi 41 | echo ${PASS} 42 | 43 | echo "${OK_COLOR}Vetting: ${NO_COLOR}" 44 | ERRS=$(go vet ${TARGETS} 2>&1 || true) 45 | if [ -n "${ERRS}" ]; then 46 | echo ${FAIL} 47 | echo "${ERRS}" 48 | exit 1 49 | fi 50 | echo ${PASS} 51 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | IMPORT_PATH: "/home/ubuntu/.go_workspace/src/github.com/rafaeljesus" 4 | APP_PATH: "$IMPORT_PATH/event-srv" 5 | pre: 6 | - curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | bash -s -- 1.10.0 7 | services: 8 | - docker 9 | - elasticsearch 10 | 11 | dependencies: 12 | pre: 13 | - docker -v 14 | - docker pull ches/kafka:0.9.0.1 15 | - docker pull jplock/zookeeper:3.4.6 16 | - sudo add-apt-repository ppa:masterminds/glide -y 17 | - sudo apt-get update 18 | - sudo apt-get install glide -y 19 | - mkdir -p "$IMPORT_PATH" 20 | override: 21 | - ln -sf "$(pwd)" "$APP_PATH" 22 | - cd "$APP_PATH" && glide install 23 | 24 | test: 25 | override: 26 | - cd "$APP_PATH" && make test 27 | deployment: 28 | master: 29 | branch: master 30 | commands: 31 | - cd "$APP_PATH" && make build 32 | - docker build -t rafaeljesus/event-srv . 33 | - docker login -e $DOCKERHUB_EMAIL -u $DOCKERHUB_USER -p $DOCKERHUB_PASS 34 | - docker tag rafaeljesus/event-srv rafaeljesus/event-srv:master 35 | - docker push rafaeljesus/event-srv:master 36 | -------------------------------------------------------------------------------- /cmd/event-srv/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | version string 12 | versionFlag bool 13 | ) 14 | 15 | func main() { 16 | versionString := "Event Service v" + version 17 | cobra.OnInitialize(func() { 18 | if versionFlag { 19 | fmt.Println(versionString) 20 | os.Exit(0) 21 | } 22 | }) 23 | 24 | var rootCmd = &cobra.Command{ 25 | Use: "event-srv", 26 | Short: "Event Service", 27 | Long: versionString, 28 | Run: Serve, 29 | } 30 | 31 | rootCmd.Flags().BoolVarP(&versionFlag, "version", "v", false, "Print application version") 32 | 33 | if err := rootCmd.Execute(); err != nil { 34 | fmt.Println(err) 35 | os.Exit(-1) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cmd/event-srv/serve.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | log "github.com/Sirupsen/logrus" 9 | "github.com/pressly/chi" 10 | "github.com/pressly/chi/middleware" 11 | "github.com/rafaeljesus/event-srv/pkg/checker" 12 | "github.com/rafaeljesus/event-srv/pkg/config" 13 | "github.com/rafaeljesus/event-srv/pkg/handlers" 14 | "github.com/rafaeljesus/event-srv/pkg/kafka-bus" 15 | m "github.com/rafaeljesus/event-srv/pkg/middleware" 16 | "github.com/rafaeljesus/event-srv/pkg/models" 17 | "github.com/rafaeljesus/event-srv/pkg/repos" 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | func Serve(cmd *cobra.Command, args []string) { 22 | log.WithField("version", version).Info("Event Service starting...") 23 | 24 | env, err := config.LoadEnv() 25 | failOnError(err, "Failed to load config!") 26 | 27 | level, err := log.ParseLevel(strings.ToLower(env.LogLevel)) 28 | failOnError(err, "Failed to get log level!") 29 | log.SetLevel(level) 30 | 31 | ds, err := datastore.New(env.DatastoreURL) 32 | failOnError(err, "Failed to init dababase connection!") 33 | defer ds.Close() 34 | 35 | e, err := kafkabus.NewEmitter(kafkabus.Config{ 36 | Url: globalConfig.BrokerURL, 37 | Attempts: globalConfig.ProducerAttempts, 38 | Timeout: globalConfig.ProducerTimeout, 39 | }) 40 | failOnError(err, "Failed to init kafka emitter connection!") 41 | defer e.Close() 42 | 43 | l, err := kafkabus.NewListener(kafkabus.Config{ 44 | Url: globalConfig.BrokerURL, 45 | }) 46 | failOnError(err, "Failed to init kafka listener connection!") 47 | defer l.Close() 48 | 49 | err := l.On("events", -1, listeners.EventCreated) 50 | failOnError(err, "Failed to init event created listener!") 51 | 52 | checkers := map[string]checker.Checker{ 53 | "api": checker.NewApi(), 54 | "elastic": checker.NewElastic(globalConfig.DatastoreURL), 55 | "kafka": checker.NewKafka(globalConfig.BrokerURL), 56 | } 57 | eventRepo := repos.NewEvent(ds) 58 | 59 | statusHandler := handlers.NewStatusHandler(checkers) 60 | eventsHandler := handlers.NewEventsHandler(eventRepo, e) 61 | 62 | r := chi.NewRouter() 63 | r.Use(middleware.RequestLogger(&m.LoggerRequest{})) 64 | r.Use(middleware.Recoverer) 65 | 66 | r.Get("/healthz", statusHandler.HealthzIndex) 67 | 68 | r.Route("/events", func(r chi.Router) { 69 | r.Get("/", eventsHandler.EventIndex) 70 | r.Post("/", eventsHandler.EventsCreate) 71 | }) 72 | 73 | log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", globalConfig.Port), r)) 74 | } 75 | 76 | func failOnError(err error, msg string) { 77 | if err != nil { 78 | log.WithError(err).Panic(msg) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | event_srv: 4 | container_name: event_srv 5 | build: . 6 | command: event-srv 7 | environment: 8 | EVENT_SRV_PORT: 3000 9 | EVENT_SRV_DB: "http://@docker:9200" 10 | EVENT_SRV_BUS: "localhost:9093" 11 | 12 | ports: 13 | - "3000:3000" 14 | depends_on: 15 | - elasticsearch 16 | - kafka 17 | 18 | elasticsearch: 19 | container_name: dev_elasticsearch 20 | image: elasticsearch:1.4.2 21 | ports: 22 | - "9200:9200" 23 | volumes: 24 | - /var/run/docker.sock:/var/run/docker.sock 25 | 26 | kafka: 27 | container_name: dev_kafka 28 | image: wurstmeister/kafka 29 | hostname: kafka 30 | environment: 31 | HOSTNAME_COMMAND: "route -n | awk '/UG[ \t]/{print $$2}'" 32 | KAFKA_ADVERTISED_PORT: 9092 33 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 34 | KAFKA_CREATE_TOPICS: "events:1:2:compact" 35 | ports: 36 | - "9093:9092" 37 | depends_on: 38 | - zookeeper 39 | 40 | zookeeper: 41 | container_name: dev_zookeeper 42 | image: oddpoet/zookeeper 43 | hostname: zookeeper 44 | command: 45 | - "2181" 46 | ports: 47 | - "2181:2181" 48 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 2950dfed234a9bc8c471b45c06fb2b3876b1a2fe145c2afdd77d09f2626f6701 2 | updated: 2017-04-23T11:41:27.395198552+02:00 3 | imports: 4 | - name: github.com/davecgh/go-spew 5 | version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 6 | subpackages: 7 | - spew 8 | - name: github.com/eapache/go-resiliency 9 | version: b86b1ec0dd4209a588dc1285cdd471e73525c0b3 10 | subpackages: 11 | - breaker 12 | - name: github.com/eapache/go-xerial-snappy 13 | version: bb955e01b9346ac19dc29eb16586c90ded99a98c 14 | - name: github.com/eapache/queue 15 | version: 44cc805cf13205b55f69e14bcb69867d1ae92f98 16 | - name: github.com/fsnotify/fsnotify 17 | version: fd9ec7deca8bf46ecd2a795baaacf2b3a9be1197 18 | - name: github.com/golang/snappy 19 | version: d9eb7a3d35ec988b8585d4a0068e462c27d28380 20 | - name: github.com/hashicorp/hcl 21 | version: 37ab263305aaeb501a60eb16863e808d426e37f2 22 | subpackages: 23 | - hcl/ast 24 | - hcl/parser 25 | - hcl/scanner 26 | - hcl/strconv 27 | - hcl/token 28 | - json/parser 29 | - json/scanner 30 | - json/token 31 | - name: github.com/inconshreveable/mousetrap 32 | version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 33 | - name: github.com/kelseyhightower/envconfig 34 | version: f611eb38b3875cc3bd991ca91c51d06446afa14c 35 | - name: github.com/klauspost/crc32 36 | version: cb6bfca970f6908083f26f39a79009d608efd5cd 37 | - name: github.com/magiconair/properties 38 | version: 9c47895dc1ce54302908ab8a43385d1f5df2c11c 39 | - name: github.com/mitchellh/mapstructure 40 | version: 5a0325d7fafaac12dda6e7fb8bd222ec1b69875e 41 | - name: github.com/pelletier/go-buffruneio 42 | version: df1e16fde7fc330a0ca68167c23bf7ed6ac31d6d 43 | - name: github.com/pelletier/go-toml 44 | version: ce7be745f09fe4ff89af8e3ea744e1deabf20ee3 45 | - name: github.com/pierrec/lz4 46 | version: 90290f74b1b4d9c097f0a3b3c7eba2ef3875c699 47 | - name: github.com/pierrec/xxHash 48 | version: 5a004441f897722c627870a981d02b29924215fa 49 | subpackages: 50 | - xxHash32 51 | - name: github.com/pressly/chi 52 | version: e6033ea75479391a4bce3918fc119cad31e1cb30 53 | subpackages: 54 | - middleware 55 | - name: github.com/rcrowley/go-metrics 56 | version: 1f30fe9094a513ce4c700b9a54458bbb0c96996c 57 | - name: github.com/Shopify/sarama 58 | version: 0fb560e5f7fbcaee2f75e3c34174320709f69944 59 | - name: github.com/Sirupsen/logrus 60 | version: ba1b36c82c5e05c4f912a88eab0dcd91a171688f 61 | - name: github.com/spf13/afero 62 | version: 2f30b2a92c0e5700bcfe4715891adb1f2a7a406d 63 | subpackages: 64 | - mem 65 | - name: github.com/spf13/cast 66 | version: 24b6558033ffe202bf42f0f3b870dcc798dd2ba8 67 | - name: github.com/spf13/cobra 68 | version: 5deb57bbca49eb370538fc295ba4b2988f9f5e09 69 | subpackages: 70 | - pflag 71 | - name: github.com/spf13/jwalterweatherman 72 | version: 33c24e77fb80341fe7130ee7c594256ff08ccc46 73 | - name: github.com/spf13/pflag 74 | version: 5ccb023bc27df288a957c5e994cd44fd19619465 75 | - name: github.com/spf13/viper 76 | version: 651d9d916abc3c3d6a91a12549495caba5edffd2 77 | - name: golang.org/x/net 78 | version: b1a2d6e8c8b5fc8f601ead62536f02a8e1b6217d 79 | subpackages: 80 | - context 81 | - context/ctxhttp 82 | - name: golang.org/x/sys 83 | version: 478fcf54317e52ab69f40bb4c7a1520288d7f7ea 84 | subpackages: 85 | - unix 86 | - name: golang.org/x/text 87 | version: 47a200a05c8b3fd1b698571caecbb68beb2611ec 88 | subpackages: 89 | - transform 90 | - unicode/norm 91 | - name: gopkg.in/olivere/elastic.v5 92 | version: 00cf81dff7fdb9e31f70e99bd909339789e71a8e 93 | subpackages: 94 | - uritemplates 95 | - name: gopkg.in/yaml.v2 96 | version: a5b47d31c556af34a302ce5d659e6fea44d90de0 97 | testImports: [] 98 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/rafaeljesus/event-srv 2 | import: 3 | - package: github.com/Shopify/sarama 4 | version: v1.11.0 5 | - package: github.com/Sirupsen/logrus 6 | version: v0.11.5 7 | - package: github.com/kelseyhightower/envconfig 8 | version: v1.3.0 9 | - package: github.com/pressly/chi 10 | version: v2.1.0 11 | subpackages: 12 | - middleware 13 | - package: github.com/spf13/cobra 14 | version: 5deb57bbca49eb370538fc295ba4b2988f9f5e09 15 | subpackages: 16 | - pflag 17 | - package: github.com/spf13/viper 18 | - package: gopkg.in/olivere/elastic.v5 19 | version: v5.0.35 20 | -------------------------------------------------------------------------------- /pkg/checker/api.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | type api struct{} 4 | 5 | func NewApi() *api { 6 | return &api{} 7 | } 8 | 9 | func (a *api) IsAlive() bool { 10 | return true 11 | } 12 | -------------------------------------------------------------------------------- /pkg/checker/checker.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | type Checker interface { 4 | IsAlive() bool 5 | } 6 | -------------------------------------------------------------------------------- /pkg/checker/elastic.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import ( 4 | "context" 5 | 6 | client "gopkg.in/olivere/elastic.v5" 7 | ) 8 | 9 | type elastic struct { 10 | url string 11 | } 12 | 13 | func NewElastic(url string) *elastic { 14 | return &elastic{url} 15 | } 16 | 17 | func (e *elastic) IsAlive() bool { 18 | url := client.SetURL(e.url) 19 | sniff := client.SetSniff(false) 20 | conn, err := client.NewClient(sniff, url) 21 | if err != nil { 22 | return false 23 | } 24 | 25 | defer conn.Stop() 26 | 27 | _, _, err = conn.Ping(e.url).Do(context.Background()) 28 | if err != nil { 29 | return false 30 | } 31 | 32 | return true 33 | } 34 | -------------------------------------------------------------------------------- /pkg/checker/kafka.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import ( 4 | "github.com/Shopify/sarama" 5 | ) 6 | 7 | type kafka struct { 8 | url string 9 | } 10 | 11 | func NewKafka(url string) *kafka { 12 | return &kafka{url} 13 | } 14 | 15 | func (k *kafka) IsAlive() bool { 16 | brokers := []string{k.url} 17 | config := sarama.NewConfig() 18 | 19 | consumer, err := sarama.NewConsumer(brokers, nil) 20 | if err != nil { 21 | return false 22 | } 23 | 24 | defer consumer.Close() 25 | 26 | producer, err := sarama.NewSyncProducer(brokers, config) 27 | if err != nil { 28 | return false 29 | } 30 | 31 | defer producer.Close() 32 | 33 | return true 34 | } 35 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/kelseyhightower/envconfig" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | type Config struct { 9 | Port int `envconfig:"PORT"` 10 | LogLevel string `envconfig:"LOG_LEVEL"` 11 | DatastoreURL string `envconfig:"DATASTORE_URL"` 12 | BrokerURL string `envconfig:"BROKER_URL"` 13 | } 14 | 15 | func init() { 16 | viper.SetDefault("port", "3000") 17 | viper.SetDefault("logLevel", "info") 18 | } 19 | 20 | func LoadEnv() (*Config, error) { 21 | var instance Config 22 | if err := viper.Unmarshal(&instance); err != nil { 23 | return nil, err 24 | } 25 | 26 | err := envconfig.Process("", &instance) 27 | if err != nil { 28 | return &instance, err 29 | } 30 | 31 | return &instance, nil 32 | } 33 | -------------------------------------------------------------------------------- /pkg/datastore/datastore.go: -------------------------------------------------------------------------------- 1 | package datastore 2 | 3 | import ( 4 | "net/url" 5 | 6 | "gopkg.in/olivere/elastic.v5" 7 | ) 8 | 9 | const ( 10 | Elastic = "elastic" 11 | ) 12 | 13 | func New(dsn string) (*elastic.Client, error) { 14 | url, err := url.Parse(dsn) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | switch url.Scheme { 20 | case Elastic: 21 | c := ESConfig{ 22 | Url: dsn, 23 | Sniff: false, 24 | } 25 | 26 | return NewElastic(c) 27 | default: 28 | return nil, ErrUnknownDatabaseProvider 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/datastore/elastic.go: -------------------------------------------------------------------------------- 1 | package datastore 2 | 3 | import ( 4 | "context" 5 | 6 | log "github.com/Sirupsen/logrus" 7 | "gopkg.in/olivere/elastic.v5" 8 | ) 9 | 10 | type ESConfig struct { 11 | Url string 12 | Sniff bool 13 | Index string 14 | } 15 | 16 | func NewElastic(c ESConfig) (conn *elastic.Client, err error) { 17 | url := elastic.SetURL(c.Url) 18 | sniff := elastic.SetSniff(c.Sniff) 19 | conn, err = elastic.NewClient(sniff, url) 20 | if err != nil { 21 | return 22 | } 23 | 24 | _, err = conn.CreateIndex(c.Index).Do(context.Background()) 25 | if err != nil { 26 | log.WithError(err).Warn("Failed to create index!") 27 | } 28 | 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /pkg/datastore/error.go: -------------------------------------------------------------------------------- 1 | package datastore 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrUnknownDatabaseProvider = errors.New("Unknown database provider type") 7 | ) 8 | -------------------------------------------------------------------------------- /pkg/handlers/events.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/rafaeljesus/event-srv/pkg/kafka-bus" 8 | "github.com/rafaeljesus/event-srv/pkg/models" 9 | "github.com/rafaeljesus/event-srv/pkg/render" 10 | "github.com/rafaeljesus/event-srv/pkg/repos" 11 | ) 12 | 13 | type EventsHandler struct { 14 | EventRepo repos.EventRepo 15 | Emitter kafkabus.Emitter 16 | } 17 | 18 | func NewEventsHandler(r repos.EventRepo, e kafkabus.Emitter) *EventsHandler { 19 | return &EventsHandler{r, e} 20 | } 21 | 22 | func (h *EventsHandler) EventsIndex(w http.ResponseWriter, r *http.Request) { 23 | from := r.URL.Query().Get("from") 24 | size := r.URL.Query().Get("size") 25 | uuid := r.URL.Query().Get("uuid") 26 | name := r.URL.Query().Get("name") 27 | status := r.URL.Query().Get("status") 28 | query := &models.Query{from, size, uuid, name, status} 29 | events, err := h.EventRepo.Find(query) 30 | if err != nil { 31 | render.JSON(w, http.StatusPreconditionFailed, err) 32 | return 33 | } 34 | 35 | render.JSON(w, http.StatusOK, events) 36 | } 37 | 38 | func (h *EventsHandler) EventsCreate(w http.ResponseWriter, r *http.Request) { 39 | event := new(models.Event) 40 | if err := json.NewDecoder(r.Body).Decode(event); err != nil { 41 | render.JSON(w, http.StatusBadRequest, "Failed to decode request body") 42 | return 43 | } 44 | 45 | h.Emitter.Emit() <- &kafkabus.Message{ 46 | Topic: "events", 47 | Payload: event, 48 | Partition: -1, 49 | } 50 | 51 | render.JSON(w, http.StatusAccepted, "OK") 52 | } 53 | -------------------------------------------------------------------------------- /pkg/handlers/events_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/pressly/chi" 11 | "github.com/rafaeljesus/event-srv/pkg/mocks" 12 | "github.com/rafaeljesus/event-srv/pkg/models" 13 | ) 14 | 15 | func TestEventsIndex(t *testing.T) { 16 | repoMock := mocks.NewEventRepo() 17 | emitterMock := mocks.NewEmitter() 18 | h := NewEventsHandler(repoMock, emitterMock) 19 | 20 | res := httptest.NewRecorder() 21 | req, err := http.NewRequest("GET", "/events", nil) 22 | if err != nil { 23 | t.Fail() 24 | } 25 | 26 | r := chi.NewRouter() 27 | r.Get("/events", h.EventsIndex) 28 | r.ServeHTTP(res, req) 29 | 30 | events := []models.Event{} 31 | if err := json.NewDecoder(res.Body).Decode(&events); err != nil { 32 | t.Fail() 33 | } 34 | 35 | if len(events) == 0 { 36 | t.Fail() 37 | } 38 | 39 | if res.Code != http.StatusOK { 40 | t.Errorf("Expected status %d to be equal %d", res.Code, http.StatusOK) 41 | } 42 | } 43 | 44 | func TestEventsIndexByUUID(t *testing.T) { 45 | repoMock := mocks.NewEventRepo() 46 | h := NewEventsHandler(repoMock, nil) 47 | 48 | res := httptest.NewRecorder() 49 | req, err := http.NewRequest("GET", "/events?uuid=12kh312uynb2u", nil) 50 | if err != nil { 51 | t.Fail() 52 | } 53 | 54 | r := chi.NewRouter() 55 | r.Get("/events", h.EventsIndex) 56 | r.ServeHTTP(res, req) 57 | 58 | events := []models.Event{} 59 | if err := json.NewDecoder(res.Body).Decode(&events); err != nil { 60 | t.Fail() 61 | } 62 | 63 | if len(events) == 0 { 64 | t.Fail() 65 | } 66 | 67 | if !repoMock.ByUUID { 68 | t.Fail() 69 | } 70 | 71 | if res.Code != http.StatusOK { 72 | t.Errorf("Expected status %d to be equal %d", res.Code, http.StatusOK) 73 | } 74 | } 75 | 76 | func TestEventsIndexByName(t *testing.T) { 77 | repoMock := mocks.NewEventRepo() 78 | h := NewEventsHandler(repoMock, nil) 79 | 80 | res := httptest.NewRecorder() 81 | req, err := http.NewRequest("GET", "/events?name=something_happened", nil) 82 | if err != nil { 83 | t.Fail() 84 | } 85 | 86 | r := chi.NewRouter() 87 | r.Get("/events", h.EventsIndex) 88 | r.ServeHTTP(res, req) 89 | 90 | events := []models.Event{} 91 | if err := json.NewDecoder(res.Body).Decode(&events); err != nil { 92 | t.Fail() 93 | } 94 | 95 | if len(events) == 0 { 96 | t.Fail() 97 | } 98 | 99 | if !repoMock.ByName { 100 | t.Fail() 101 | } 102 | 103 | if res.Code != http.StatusOK { 104 | t.Errorf("Expected status %d to be equal %d", res.Code, http.StatusOK) 105 | } 106 | } 107 | 108 | func TestEventsIndexByStatus(t *testing.T) { 109 | repoMock := mocks.NewEventRepo() 110 | h := NewEventsHandler(repoMock, nil) 111 | 112 | res := httptest.NewRecorder() 113 | req, err := http.NewRequest("GET", "/events?status=something_processed", nil) 114 | if err != nil { 115 | t.Fail() 116 | } 117 | 118 | r := chi.NewRouter() 119 | r.Get("/events", h.EventsIndex) 120 | r.ServeHTTP(res, req) 121 | 122 | events := []models.Event{} 123 | if err := json.NewDecoder(res.Body).Decode(&events); err != nil { 124 | t.Fail() 125 | } 126 | 127 | if len(events) == 0 { 128 | t.Fail() 129 | } 130 | 131 | if !repoMock.ByStatus { 132 | t.Fail() 133 | } 134 | 135 | if res.Code != http.StatusOK { 136 | t.Errorf("Expected status %d to be equal %d", res.Code, http.StatusOK) 137 | } 138 | } 139 | 140 | func TestEventsCreate(t *testing.T) { 141 | repoMock := mocks.NewEventRepo() 142 | emitterMock := mocks.NewEmitter() 143 | h := NewEventsHandler(repoMock, emitterMock) 144 | 145 | res := httptest.NewRecorder() 146 | body := strings.NewReader(`{"name":"foo","status":"bar","payload":"some_data"}`) 147 | req, err := http.NewRequest("POST", "/events", body) 148 | if err != nil { 149 | t.Fail() 150 | } 151 | 152 | r := chi.NewRouter() 153 | r.Post("/events", h.EventsCreate) 154 | r.ServeHTTP(res, req) 155 | 156 | if !repoMock.Created { 157 | t.Fail() 158 | } 159 | 160 | if !emitterMock.Emitted { 161 | t.Fail() 162 | } 163 | 164 | if res.Code != http.StatusAccepted { 165 | t.Errorf("Expected status %d to be equal %d", res.Code, http.StatusAccepted) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /pkg/handlers/healthz.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/rafaeljesus/event-srv/pkg/checker" 7 | "github.com/rafaeljesus/event-srv/pkg/render" 8 | ) 9 | 10 | type HealthzHandler struct { 11 | checkers map[string]checker.Checker 12 | } 13 | 14 | func NewHealthzHandler(checkers map[string]checker.Checker) *HealthzHandler { 15 | return &HealthzHandler{checkers} 16 | } 17 | 18 | func (h *HealthzHandler) HealthzIndex(w http.ResponseWriter, r *http.Request) { 19 | payload := make(map[string]bool) 20 | 21 | for k, v := range h.checkers { 22 | payload[k] = v.IsAlive() 23 | } 24 | 25 | render.JSON(w, http.StatusOK, payload) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/handlers/healthz_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/pressly/chi" 10 | "github.com/rafaeljesus/event-srv/pkg/checker" 11 | "github.com/rafaeljesus/event-srv/pkg/mocks" 12 | ) 13 | 14 | func TestHealthzIndex(t *testing.T) { 15 | checkers := map[string]checker.Checker{ 16 | "api": mocks.NewCheckerMock(), 17 | "elastic": mocks.NewCheckerMock(), 18 | "kafka": mocks.NewCheckerMock(), 19 | } 20 | h := NewHealthzHandler(checkers) 21 | 22 | res := httptest.NewRecorder() 23 | req, err := http.NewRequest("GET", "/healthz", nil) 24 | if err != nil { 25 | t.Fail() 26 | } 27 | 28 | r := chi.NewRouter() 29 | r.Get("/healthz", h.HealthzIndex) 30 | r.ServeHTTP(res, req) 31 | 32 | response := make(map[string]bool) 33 | if err := json.NewDecoder(res.Body).Decode(&response); err != nil { 34 | t.Fail() 35 | } 36 | 37 | if response["api"] != true { 38 | t.Fail() 39 | } 40 | 41 | if response["elastic"] != true { 42 | t.Fail() 43 | } 44 | 45 | if response["kafka"] != true { 46 | t.Fail() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/kafka-bus/config.go: -------------------------------------------------------------------------------- 1 | package kafkabus 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Config struct { 8 | Url string 9 | Attempts int 10 | Timeout time.Duration 11 | } 12 | -------------------------------------------------------------------------------- /pkg/kafka-bus/emitter.go: -------------------------------------------------------------------------------- 1 | package kafkabus 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/Shopify/sarama" 7 | log "github.com/Sirupsen/logrus" 8 | ) 9 | 10 | type Emitter interface { 11 | Emit() chan *Message 12 | Close() 13 | } 14 | 15 | type kafkaEmitter struct { 16 | emitter sarama.AsyncProducer 17 | emitterChan chan *Message 18 | } 19 | 20 | func NewEmitter(c Config) (emitter Emitter, err error) { 21 | brokers := []string{c.Url} 22 | config := sarama.NewConfig() 23 | config.Producer.Retry.Max = c.Attempts 24 | config.Producer.Partitioner = sarama.NewRandomPartitioner 25 | config.Producer.RequiredAcks = sarama.WaitForAll 26 | config.Producer.Return.Successes = true 27 | config.Producer.Return.Errors = true 28 | 29 | producer, err := sarama.NewAsyncProducer(brokers, nil) 30 | if err != nil { 31 | return 32 | } 33 | 34 | ke := &kafkaEmitter{ 35 | emitter: producer, 36 | emitterChan: make(chan *Message), 37 | } 38 | 39 | go ke.register() 40 | 41 | emitter = ke 42 | 43 | return 44 | } 45 | 46 | func (ke *kafkaEmitter) Emit() chan *Message { 47 | return ke.emitterChan 48 | } 49 | 50 | func (ke *kafkaEmitter) register() { 51 | for { 52 | select { 53 | case m := <-ke.emitterChan: 54 | ke.emit(m) 55 | } 56 | } 57 | } 58 | 59 | func (ke *kafkaEmitter) emit(m *Message) (err error) { 60 | l := log.WithFields(log.Fields{ 61 | "topic": m.Topic, 62 | "partition": m.Partition, 63 | }) 64 | 65 | l.Debug("Sending event") 66 | 67 | p, err := json.Marshal(m.Payload) 68 | if err != nil { 69 | return 70 | } 71 | 72 | message := &sarama.ProducerMessage{ 73 | Topic: m.Topic, 74 | Partition: m.Partition, 75 | Value: sarama.StringEncoder(p), 76 | } 77 | 78 | for { 79 | select { 80 | case ke.emitter.Input() <- message: 81 | case <-ke.emitter.Successes(): 82 | l.Info("Event successfully sent") 83 | return 84 | case errors := <-ke.emitter.Errors(): 85 | l.WithError(err).Error("Failed to send event") 86 | err = errors 87 | return 88 | } 89 | } 90 | } 91 | 92 | func (ke *kafkaEmitter) Close() { 93 | ke.emitter.Close() 94 | } 95 | -------------------------------------------------------------------------------- /pkg/kafka-bus/listener.go: -------------------------------------------------------------------------------- 1 | package kafkabus 2 | 3 | import ( 4 | "github.com/Shopify/sarama" 5 | log "github.com/Sirupsen/logrus" 6 | ) 7 | 8 | type Listener interface { 9 | On(topic string, partition int32, fn fnHandler) error 10 | Close() 11 | } 12 | 13 | type kafkaListener struct { 14 | listener sarama.Consumer 15 | } 16 | 17 | type fnHandler func(payload []byte) error 18 | 19 | func NewListener(c Config) (listener Listener, err error) { 20 | brokers := []string{c.Url} 21 | config := sarama.NewConfig() 22 | config.Consumer.Return.Errors = true 23 | 24 | consumer, err := sarama.NewConsumer(brokers, config) 25 | if err != nil { 26 | return 27 | } 28 | 29 | listener = &kafkaListener{consumer} 30 | 31 | return 32 | } 33 | 34 | func (kl *kafkaListener) On(topic string, partition int32, fn fnHandler) (err error) { 35 | pc, err := kl.listener.ConsumePartition(topic, partition, sarama.OffsetOldest) 36 | if err != nil { 37 | return 38 | } 39 | 40 | go func() { 41 | for { 42 | select { 43 | case message := <-pc.Messages(): 44 | log.WithFields(log.Fields{ 45 | "topic": topic, 46 | "partition": partition, 47 | }).Debug("receiving a message from broker") 48 | 49 | fn(message.Value) 50 | } 51 | } 52 | }() 53 | 54 | return 55 | } 56 | 57 | func (kl *kafkaListener) Close() { 58 | kl.listener.Close() 59 | } 60 | -------------------------------------------------------------------------------- /pkg/kafka-bus/message.go: -------------------------------------------------------------------------------- 1 | package kafkabus 2 | 3 | type Message struct { 4 | Topic string 5 | Payload interface{} 6 | Partition int32 7 | } 8 | -------------------------------------------------------------------------------- /pkg/middleware/logger_request.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | log "github.com/Sirupsen/logrus" 9 | "github.com/pressly/chi/middleware" 10 | ) 11 | 12 | type LoggerRequest struct{} 13 | 14 | type LoggerEntry struct { 15 | logger log.FieldLogger 16 | } 17 | 18 | func (l *LoggerRequest) NewLogEntry(r *http.Request) middleware.LogEntry { 19 | entry := &LoggerEntry{} 20 | 21 | entry.logger = log.WithFields(log.Fields{ 22 | "method": r.Method, 23 | "path": r.URL.Path, 24 | "remote-addr": r.RemoteAddr, 25 | "user-agent": r.UserAgent(), 26 | }) 27 | 28 | entry.logger.Debug("Start serving request") 29 | 30 | return entry 31 | } 32 | 33 | func (l *LoggerEntry) Write(status, bytes int, elapsed time.Duration) { 34 | l.logger = l.logger.WithFields(log.Fields{ 35 | "code": status, 36 | "bytes_length": bytes, 37 | "elapsed_ms": float64(elapsed.Nanoseconds()) / 1000000.0, 38 | }) 39 | 40 | l.logger.Infoln("Finished serving request") 41 | } 42 | 43 | func (l *LoggerEntry) Panic(v interface{}, stack []byte) { 44 | l.logger = l.logger.WithFields(log.Fields{ 45 | "stack": string(stack), 46 | "panic": fmt.Sprintf("%+v", v), 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/mocks/checker_mock.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | type CheckerMock struct{} 4 | 5 | func NewCheckerMock() *CheckerMock { 6 | return &CheckerMock{} 7 | } 8 | 9 | func (c *CheckerMock) IsAlive() bool { 10 | return true 11 | } 12 | -------------------------------------------------------------------------------- /pkg/mocks/emitter_mock.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/rafaeljesus/event-srv/pkg/kafka-bus" 5 | ) 6 | 7 | type EmitterMock struct { 8 | Emitted bool 9 | c chan *kafkabus.Message 10 | } 11 | 12 | func NewEmitter() *EmitterMock { 13 | return &EmitterMock{ 14 | c: make(chan *kafkabus.Message), 15 | } 16 | } 17 | 18 | func (e *EmitterMock) Emit() chan *kafkabus.Message { 19 | e.Emitted = true 20 | go func() { 21 | <-e.c 22 | }() 23 | return e.c 24 | } 25 | 26 | func (e *EmitterMock) Close() {} 27 | -------------------------------------------------------------------------------- /pkg/mocks/event_repo_mock.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/rafaeljesus/event-srv/pkg/models" 5 | ) 6 | 7 | type EventRepoMock struct { 8 | Created bool 9 | Found bool 10 | ByUUID bool 11 | ByName bool 12 | ByStatus bool 13 | } 14 | 15 | func NewEventRepo() *EventRepoMock { 16 | return &EventRepoMock{ 17 | Created: false, 18 | Found: false, 19 | ByUUID: false, 20 | ByName: false, 21 | ByStatus: false, 22 | } 23 | } 24 | 25 | func (repo *EventRepoMock) Create(event *models.Event) (err error) { 26 | repo.Created = true 27 | return 28 | } 29 | 30 | func (repo *EventRepoMock) Find(sc *models.Query) (events []models.Event, err error) { 31 | events = append(events, models.Event{ 32 | UUID: "12kh312uynb2u", 33 | Name: "something_happened", 34 | Status: "something_processed", 35 | }) 36 | 37 | switch true { 38 | case sc.UUID != "": 39 | repo.ByUUID = true 40 | return 41 | case sc.Name != "": 42 | repo.ByName = true 43 | return 44 | case sc.Status != "": 45 | repo.ByStatus = true 46 | return 47 | default: 48 | repo.Found = true 49 | return 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/models/event.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | type Event struct { 9 | UUID string `json:"uuid"` 10 | Name string `json:"name"` 11 | Status string `json:"status"` 12 | Payload *json.RawMessage `json:"payload"` 13 | OcurredOn time.Time `json:"ocurred_on,omitempty"` 14 | } 15 | -------------------------------------------------------------------------------- /pkg/models/query.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Query struct { 4 | From string 5 | Size string 6 | UUID string 7 | Name string 8 | Status string 9 | } 10 | -------------------------------------------------------------------------------- /pkg/render/render.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | func JSON(w http.ResponseWriter, code int, v interface{}) (err error) { 9 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 10 | w.WriteHeader(code) 11 | 12 | if v == nil || code == http.StatusNoContent { 13 | return 14 | } 15 | 16 | err = encode(w, v) 17 | 18 | return 19 | } 20 | 21 | func encode(w http.ResponseWriter, v interface{}) (err error) { 22 | enc := json.NewEncoder(w) 23 | enc.SetEscapeHTML(true) 24 | 25 | if err = enc.Encode(v); err != nil { 26 | http.Error(w, err.Error(), http.StatusInternalServerError) 27 | } 28 | 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /pkg/repos/event.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "strconv" 7 | 8 | "github.com/rafaeljesus/event-srv/pkg/models" 9 | "gopkg.in/olivere/elastic.v5" 10 | ) 11 | 12 | type EventRepo interface { 13 | Create(e *models.Event) error 14 | Find(q *models.Query) ([]models.Event, error) 15 | } 16 | 17 | type Event struct { 18 | db *elastic.Client 19 | } 20 | 21 | func NewEvent(db *elastic.Client) *Event { 22 | return &Event{db} 23 | } 24 | 25 | func (e *Event) Create(event *models.Event) (err error) { 26 | _, err = e.db.Index(). 27 | Index("events"). 28 | Type("event"). 29 | BodyJson(e). 30 | Refresh("true"). 31 | Do(context.Background()) 32 | 33 | return 34 | } 35 | 36 | func (e *Event) Find(q *models.Query) (events []models.Event, err error) { 37 | index := e.db.Search().Index("events") 38 | 39 | from, err := strconv.Atoi(q.From) 40 | if err != nil { 41 | from = 0 42 | } 43 | 44 | size, err := strconv.Atoi(q.Size) 45 | if err != nil { 46 | size = 10 47 | } 48 | 49 | if q.UUID != "" { 50 | cid := elastic.NewTermQuery("cid", q.UUID) 51 | index.Query(cid) 52 | } 53 | 54 | if q.Name != "" { 55 | name := elastic.NewTermQuery("name", q.Name) 56 | index.Query(name) 57 | } 58 | 59 | if q.Status != "" { 60 | status := elastic.NewTermQuery("status", q.Status) 61 | index.Query(status) 62 | } 63 | 64 | searchResult, err := index. 65 | Sort("ocurred_on", true). 66 | From(from). 67 | Size(size). 68 | Do(context.Background()) 69 | 70 | if err != nil { 71 | return 72 | } 73 | 74 | events, err = parseResult(searchResult) 75 | 76 | return 77 | } 78 | 79 | func parseResult(searchResult *elastic.SearchResult) (events []models.Event, err error) { 80 | for _, hit := range searchResult.Hits.Hits { 81 | var event models.Event 82 | json.Unmarshal(*hit.Source, &event) 83 | events = append(events, event) 84 | } 85 | 86 | return 87 | } 88 | --------------------------------------------------------------------------------