├── .gitignore ├── README.md ├── adapters ├── config │ ├── config.go │ ├── dynamic.go │ └── static.go ├── http │ ├── auth.go │ ├── device │ │ └── add_device.go │ ├── docs │ │ ├── docs.go │ │ ├── swagger.json │ │ └── swagger.yaml │ ├── health │ │ └── health.go │ ├── metrics │ │ ├── decimal.go │ │ ├── metrics.go │ │ └── temperature.go │ └── server.go ├── jwt │ ├── device.go │ └── jwt.go └── storage │ └── memory │ ├── ingestion.go │ └── registry.go ├── cmd └── server │ ├── main.go │ ├── wire.go │ └── wire_gen.go ├── go.mod ├── go.sum ├── mocks └── add_metrics.go ├── pkg ├── auth │ └── device.go ├── device │ ├── add-device │ │ └── add_device.go │ └── registry │ │ ├── device.go │ │ └── register.go └── metrics │ ├── add-metrics │ ├── add_metrics.go │ └── add_metrics_test.go │ ├── alert │ ├── alert.go │ └── alert_test.go │ ├── ingestion │ ├── metric.go │ └── time.go │ └── query-metrics │ └── query_metrics.go └── resources ├── config.yaml └── config_dynamic.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/go,vim,macos,intellij+all 3 | # Edit at https://www.gitignore.io/?templates=go,vim,macos,intellij+all 4 | 5 | ### Go ### 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Dependency directories (remove the comment below to include it) 20 | # vendor/ 21 | 22 | ### Go Patch ### 23 | /vendor/ 24 | /Godeps/ 25 | 26 | ### Intellij+all ### 27 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 28 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 29 | 30 | # User-specific stuff 31 | .idea/**/workspace.xml 32 | .idea/**/tasks.xml 33 | .idea/**/usage.statistics.xml 34 | .idea/**/dictionaries 35 | .idea/**/shelf 36 | 37 | # Generated files 38 | .idea/**/contentModel.xml 39 | 40 | # Sensitive or high-churn files 41 | .idea/**/dataSources/ 42 | .idea/**/dataSources.ids 43 | .idea/**/dataSources.local.xml 44 | .idea/**/sqlDataSources.xml 45 | .idea/**/dynamic.xml 46 | .idea/**/uiDesigner.xml 47 | .idea/**/dbnavigator.xml 48 | 49 | # Gradle 50 | .idea/**/gradle.xml 51 | .idea/**/libraries 52 | 53 | # Gradle and Maven with auto-import 54 | # When using Gradle or Maven with auto-import, you should exclude module files, 55 | # since they will be recreated, and may cause churn. Uncomment if using 56 | # auto-import. 57 | # .idea/modules.xml 58 | # .idea/*.iml 59 | # .idea/modules 60 | # *.iml 61 | # *.ipr 62 | 63 | # CMake 64 | cmake-build-*/ 65 | 66 | # Mongo Explorer plugin 67 | .idea/**/mongoSettings.xml 68 | 69 | # File-based project format 70 | *.iws 71 | 72 | # IntelliJ 73 | out/ 74 | 75 | # mpeltonen/sbt-idea plugin 76 | .idea_modules/ 77 | 78 | # JIRA plugin 79 | atlassian-ide-plugin.xml 80 | 81 | # Cursive Clojure plugin 82 | .idea/replstate.xml 83 | 84 | # Crashlytics plugin (for Android Studio and IntelliJ) 85 | com_crashlytics_export_strings.xml 86 | crashlytics.properties 87 | crashlytics-build.properties 88 | fabric.properties 89 | 90 | # Editor-based Rest Client 91 | .idea/httpRequests 92 | 93 | # Android studio 3.1+ serialized cache file 94 | .idea/caches/build_file_checksums.ser 95 | 96 | ### Intellij+all Patch ### 97 | # Ignores the whole .idea folder and all .iml files 98 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 99 | 100 | .idea/ 101 | 102 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 103 | 104 | *.iml 105 | modules.xml 106 | .idea/misc.xml 107 | *.ipr 108 | 109 | # Sonarlint plugin 110 | .idea/sonarlint 111 | 112 | ### macOS ### 113 | # General 114 | .DS_Store 115 | .AppleDouble 116 | .LSOverride 117 | 118 | # Icon must end with two \r 119 | Icon 120 | 121 | # Thumbnails 122 | ._* 123 | 124 | # Files that might appear in the root of a volume 125 | .DocumentRevisions-V100 126 | .fseventsd 127 | .Spotlight-V100 128 | .TemporaryItems 129 | .Trashes 130 | .VolumeIcon.icns 131 | .com.apple.timemachine.donotpresent 132 | 133 | # Directories potentially created on remote AFP share 134 | .AppleDB 135 | .AppleDesktop 136 | Network Trash Folder 137 | Temporary Items 138 | .apdisk 139 | 140 | ### Vim ### 141 | # Swap 142 | [._]*.s[a-v][a-z] 143 | [._]*.sw[a-p] 144 | [._]s[a-rt-v][a-z] 145 | [._]ss[a-gi-z] 146 | [._]sw[a-p] 147 | 148 | # Session 149 | Session.vim 150 | Sessionx.vim 151 | 152 | # Temporary 153 | .netrwhist 154 | *~ 155 | 156 | # Auto-generated tag files 157 | tags 158 | 159 | # Persistent undo 160 | [._]*.un~ 161 | 162 | # Coc configuration directory 163 | .vim 164 | 165 | # End of https://www.gitignore.io/api/go,vim,macos,intellij+all -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iot-demo 2 | This repository demonstrates tools that can solve some fundamental problems with developing REST APIs in GoLang. Mostly using code generation. 3 | 4 | ## Toy Project 5 | Will keep track of device metrics in a factory. Devices will periodically send metric data. According to the metric sent by the device, we may want to make the device sound an alert. We also want to be able to see metrics sent by a given device. 6 | 7 | - Devices will be registered with serial number, registration date and a firmware version. They will be given a token on registration. 8 | - Each device will send temperature data as a double value, using the token supplied on registration. 9 | - When device sends a metric data, it may receive 'ALERT' or 'OK' message. 10 | - We want to give an alert on a certain thresholds(initially between 70-100C). We want this configurable. 11 | - We want an endpoint that accepts device id and returns metrics. 12 | 13 | ## Tools Demonstrated 14 | - [wire](https://github.com/google/wire) for dependency injection with code generation 15 | - [swag](https://github.com/swaggo/swag) for documentation generation 16 | - [mockgen](https://github.com/golang/mock) for generating mock structs for interfaces 17 | - [viper](https://github.com/spf13/viper) for static/dynamic configuration. we use configmap mount with kubernetes in production. dynamic configuration works as is. 18 | 19 | 20 | ## Run the Server 21 | Just run `go mod download` and then `go run iot-demo/cmd/server` to start the project. By default it listens to 8080 port. You can override it by setting the PORT env variable. 22 | 23 | ## Dependencies 24 | To install dependencies used by the project, run `go mod download`. There are also some global dependencies that you may need to install to make `go generate ./...` run. Those are: 25 | 26 | ``` 27 | # for di 28 | go get github.com/google/wire/cmd/wire 29 | # for test mocks 30 | go get github.com/golang/mock/gomock 31 | go install github.com/golang/mock/mockgen 32 | # install swag for doc generation 33 | go get github.com/swaggo/swag/cmd/swag 34 | ``` 35 | 36 | ## Environment Variables 37 | - `PROFILE` decides which environments configurations should override default configuration. Could be dev/stage/prod. 38 | - `GO_ENV` can be production. Controls the `IsRelease` flag in global configuration. 39 | - `PORT` decides which port to start the server on. 40 | 41 | ## Project Structure 42 | ``` 43 | . 44 | ├── adapters # implementation details 45 | │   ├── config # static/dynamic config reading with viper 46 | │   ├── http # REST HTTP API with gin, documented with swag 47 | │   ├── jwt # jwt creation/parsing with jwt-go 48 | │   └── storage # in memory repository functions (NOT THREAD SAFE!) 49 | ├── mocks # test struct mocks for interfaces 50 | ├── resources # configuration files 51 | │ ├── config.yaml # static configuration file 52 | │ └── config_dynamic.yaml # dynamic configuration file that will be re-read on changes 53 | ├── cmd 54 | │   └── server # server entry point with wire 55 | └─── pkg # business logic 56 | ├── auth 57 | ├── device 58 | │   ├── add-device # device registration use-case 59 | │   └── registry # device CRUD service 60 | └── metrics 61 | ├── query-metrics # metric query use-case 62 | ├── add-metrics # metric insert use-case 63 |    ├── ingestion # metric CRUD service 64 |   └── alert # threshold alert business logic 65 | 66 | ``` 67 | 68 | ## Some Questions Related to the Structure 69 | - Is it okay to put the mocks under `/mocks`. Any better place to put it? 70 | - `adapters` folder includes implementation details of config/auth/http/repository. Should they be put under `/pkg` if so where? Putting them side by side with business logic, makes code harder to navigate. 71 | - Better way of naming business logic related packages? Package under `/pkg/device/registry` is named `registry`. should it be named something else? 72 | 73 | -------------------------------------------------------------------------------- /adapters/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Selection struct { 4 | StaticConfigurationFilePath string 5 | DynamicConfigurationFilePath string 6 | Profile string 7 | } 8 | 9 | type Static struct { 10 | Server struct { 11 | Name string 12 | Host string 13 | Port int 14 | } 15 | Swagger struct { 16 | DocumentationHost string 17 | DocumentationBasePath string 18 | } 19 | Auth struct { 20 | Secret string 21 | } 22 | IsRelease bool 23 | } 24 | 25 | type Dynamic struct { 26 | Threshold struct { 27 | Min float64 28 | Max float64 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /adapters/config/dynamic.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "github.com/fsnotify/fsnotify" 6 | "github.com/prometheus/common/log" 7 | "github.com/spf13/viper" 8 | "iot-demo/pkg/metrics/alert" 9 | "sync" 10 | ) 11 | 12 | type DynamicGetter struct { 13 | viperInstance *viper.Viper 14 | mutex *sync.RWMutex 15 | config *Dynamic 16 | } 17 | 18 | 19 | func (dg *DynamicGetter) GetThreshold() alert.Threshold { 20 | return dg.getCurrent().Threshold 21 | } 22 | 23 | func NewDynamicGetter(s Selection) (*DynamicGetter, error) { 24 | viperInstance := viper.New() 25 | viperInstance.SetConfigFile(s.DynamicConfigurationFilePath) 26 | viperInstance.SetTypeByDefaultValue(true) 27 | err := viperInstance.ReadInConfig() 28 | if err != nil { 29 | return nil, errors.New("config could not be found") 30 | } 31 | viperInstance.WatchConfig() 32 | dg := &DynamicGetter{viperInstance, &sync.RWMutex{}, nil} 33 | 34 | viperInstance.OnConfigChange(func(e fsnotify.Event) { 35 | if err := dg.Update(); err != nil { 36 | log.Error("an error happened when updating the config: " + err.Error()) 37 | } 38 | }) 39 | 40 | return dg, dg.Update() 41 | } 42 | 43 | func (dg *DynamicGetter) Update() error { 44 | var cfg Dynamic 45 | err := dg.viperInstance.Unmarshal(&cfg) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | dg.mutex.Lock() 51 | defer dg.mutex.Unlock() 52 | dg.config = &cfg 53 | return nil 54 | } 55 | 56 | func (dg *DynamicGetter) getCurrent() Dynamic { 57 | dg.mutex.RLock() 58 | defer dg.mutex.RUnlock() 59 | return *dg.config 60 | } 61 | -------------------------------------------------------------------------------- /adapters/config/static.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "github.com/spf13/viper" 6 | "os" 7 | ) 8 | 9 | func ReadStatic(s Selection) (Static, error) { 10 | viperInstance := viper.New() 11 | viperInstance.SetConfigFile(s.StaticConfigurationFilePath) 12 | 13 | err := viperInstance.ReadInConfig() 14 | if err != nil { 15 | return Static{}, err 16 | } 17 | 18 | cfg, err := createSubConfiguration(viperInstance, s.Profile) 19 | if err != nil { 20 | return Static{}, err 21 | } 22 | 23 | cfg.IsRelease = os.Getenv("GO_ENV") == "production" 24 | return cfg, err 25 | } 26 | 27 | func createSubConfiguration(viperInstance *viper.Viper, sub string) (Static, error) { 28 | configuration := Static{} 29 | subEnvironment := viperInstance.Sub(sub) 30 | if subEnvironment == nil { 31 | return Static{}, errors.New("no config found for the given environment") 32 | } 33 | 34 | defaultEnvironment := viperInstance.Sub("default") 35 | if defaultEnvironment == nil { 36 | return Static{}, errors.New("no config found for the default environment") 37 | } 38 | 39 | defaultEnvironment.MergeConfigMap(subEnvironment.AllSettings()) 40 | 41 | defaultEnvironment.BindEnv("server.port", "PORT") 42 | 43 | err := defaultEnvironment.Unmarshal(&configuration) 44 | if err != nil { 45 | return Static{}, nil 46 | } 47 | return configuration, nil 48 | } 49 | -------------------------------------------------------------------------------- /adapters/http/auth.go: -------------------------------------------------------------------------------- 1 | package http_server 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "iot-demo/pkg/auth" 6 | "strings" 7 | ) 8 | 9 | type DeviceTokenParser interface { 10 | Parse(authToken auth.Token) (*auth.DeviceCredential, error) 11 | } 12 | 13 | func deviceAuthParserHandler(parser DeviceTokenParser) gin.HandlerFunc { 14 | return func(c *gin.Context) { 15 | header := c.GetHeader("authorization") 16 | if header == "" { 17 | c.Next() 18 | return 19 | } 20 | 21 | split := strings.SplitN(header, " ", 2) 22 | if len(split) != 2 || split[0] != "Bearer" { 23 | c.Next() 24 | return 25 | } 26 | 27 | token := auth.Token(strings.TrimSpace(split[1])) 28 | authInfo, err := parser.Parse(token) 29 | 30 | if err != nil { 31 | c.String(401, "bad authentication token") 32 | c.Abort() 33 | return 34 | } 35 | 36 | c.Set("auth_info", authInfo) 37 | c.Next() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /adapters/http/device/add_device.go: -------------------------------------------------------------------------------- 1 | package http_device 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | add_device "iot-demo/pkg/device/add-device" 6 | "iot-demo/pkg/device/registry" 7 | "time" 8 | ) 9 | 10 | type deviceRegisterRequestDTO struct { 11 | SerialNumber string `json:"serial_number" example:"TEST-123"` 12 | FirmwareVersion string `json:"firmware_version" example:"1.0.0-1"` 13 | } 14 | 15 | type deviceRegisterResponseDTO struct { 16 | Device *registry.Device `json:"device"` 17 | Token string `json:"token"` 18 | } 19 | 20 | const ( 21 | invalidRegisterRequestMessage = "please supply serial_number and firmware_version" 22 | ) 23 | 24 | // DeviceRegistry godoc 25 | // @Summary register a new device 26 | // @Description register a new device with the given parameters 27 | // @Tags device 28 | // @Accept json 29 | // @Produce json 30 | // @Param device body http_device.deviceRegisterRequestDTO true "info of the device to register" 31 | // @Success 201 {object} http_device.deviceRegisterResponseDTO "created new device" 32 | // @Failure 400 {string} string "invalid request parameters" 33 | // @Failure 500 {string} string "unexpected error occurred" 34 | // @Router /device [post] 35 | func makeAddDeviceHandler(addDevice *add_device.Service) gin.HandlerFunc { 36 | return func(c *gin.Context) { 37 | var requestDTO deviceRegisterRequestDTO 38 | if err := c.BindJSON(&requestDTO); err != nil { 39 | c.String(400, invalidRegisterRequestMessage) 40 | return 41 | } 42 | 43 | device, token, err := addDevice.Register(requestDTO.SerialNumber, requestDTO.FirmwareVersion, time.Now()) 44 | if err != nil { 45 | c.JSON(500, err.Error()) 46 | return 47 | } 48 | 49 | c.JSON(200, deviceRegisterResponseDTO{ 50 | Device: device, 51 | Token: string(token), 52 | }) 53 | return 54 | } 55 | } 56 | 57 | type Handlers struct { 58 | Service *add_device.Service 59 | } 60 | 61 | func (drh Handlers) Register(engine *gin.Engine) { 62 | engine.POST("/device", makeAddDeviceHandler(drh.Service)) 63 | } 64 | -------------------------------------------------------------------------------- /adapters/http/docs/docs.go: -------------------------------------------------------------------------------- 1 | // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT 2 | // This file was generated by swaggo/swag at 3 | // 2020-02-21 14:53:04.388095 +0300 +03 m=+4.464948415 4 | 5 | package docs 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "strings" 11 | 12 | "github.com/alecthomas/template" 13 | "github.com/swaggo/swag" 14 | ) 15 | 16 | var doc = `{ 17 | "schemes": {{ marshal .Schemes }}, 18 | "swagger": "2.0", 19 | "info": { 20 | "description": "{{.Description}}", 21 | "title": "{{.Title}}", 22 | "contact": { 23 | "name": "Yengas", 24 | "email": "yigitcan.ucum@trendyol.com" 25 | }, 26 | "license": { 27 | "name": "Apache 2.0", 28 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 29 | }, 30 | "version": "{{.Version}}" 31 | }, 32 | "host": "{{.Host}}", 33 | "basePath": "{{.BasePath}}", 34 | "paths": { 35 | "/_monitoring/health": { 36 | "get": { 37 | "description": "returns ok if the server is up", 38 | "consumes": [ 39 | "application/json" 40 | ], 41 | "produces": [ 42 | "application/json" 43 | ], 44 | "tags": [ 45 | "health" 46 | ], 47 | "summary": "get status of the server", 48 | "responses": { 49 | "200": { 50 | "description": "ok", 51 | "schema": { 52 | "type": "string" 53 | } 54 | } 55 | } 56 | } 57 | }, 58 | "/device": { 59 | "post": { 60 | "description": "register a new device with the given parameters", 61 | "consumes": [ 62 | "application/json" 63 | ], 64 | "produces": [ 65 | "application/json" 66 | ], 67 | "tags": [ 68 | "device" 69 | ], 70 | "summary": "register a new device", 71 | "parameters": [ 72 | { 73 | "description": "info of the device to register", 74 | "name": "device", 75 | "in": "body", 76 | "required": true, 77 | "schema": { 78 | "$ref": "#/definitions/http_device.deviceRegisterRequestDTO" 79 | } 80 | } 81 | ], 82 | "responses": { 83 | "201": { 84 | "description": "created new device", 85 | "schema": { 86 | "$ref": "#/definitions/http_device.deviceRegisterResponseDTO" 87 | } 88 | }, 89 | "400": { 90 | "description": "invalid request parameters", 91 | "schema": { 92 | "type": "string" 93 | } 94 | }, 95 | "500": { 96 | "description": "unexpected error occurred", 97 | "schema": { 98 | "type": "string" 99 | } 100 | } 101 | } 102 | } 103 | }, 104 | "/metric/temperature": { 105 | "get": { 106 | "description": "given a device and a starting date, returns all temperature metrics", 107 | "consumes": [ 108 | "application/json" 109 | ], 110 | "produces": [ 111 | "application/json" 112 | ], 113 | "tags": [ 114 | "metric" 115 | ], 116 | "summary": "query temperature metrics of devices", 117 | "parameters": [ 118 | { 119 | "type": "string", 120 | "description": "id of the device", 121 | "name": "deviceID", 122 | "in": "query", 123 | "required": true 124 | } 125 | ], 126 | "responses": { 127 | "200": { 128 | "description": "metrics matching the criteria", 129 | "schema": { 130 | "type": "array", 131 | "items": { 132 | "$ref": "#/definitions/ingestion.DecimalMetricValue" 133 | } 134 | } 135 | }, 136 | "404": { 137 | "description": "no metrics found", 138 | "schema": { 139 | "type": "string" 140 | } 141 | }, 142 | "500": { 143 | "description": "unexpected error occurred", 144 | "schema": { 145 | "type": "string" 146 | } 147 | } 148 | } 149 | }, 150 | "post": { 151 | "security": [ 152 | { 153 | "ApiKeyAuth": [] 154 | } 155 | ], 156 | "description": "inserts temperature metric data for the given device id", 157 | "consumes": [ 158 | "application/json" 159 | ], 160 | "produces": [ 161 | "application/json" 162 | ], 163 | "tags": [ 164 | "metric" 165 | ], 166 | "summary": "insert temperature metric data for devices", 167 | "parameters": [ 168 | { 169 | "description": "metrics to insert", 170 | "name": "metrics", 171 | "in": "body", 172 | "required": true, 173 | "schema": { 174 | "$ref": "#/definitions/ingestion.DecimalMetricValueList" 175 | } 176 | } 177 | ], 178 | "responses": { 179 | "201": { 180 | "description": "inserted the temperature metrics", 181 | "schema": { 182 | "type": "string" 183 | } 184 | }, 185 | "400": { 186 | "description": "invalid request parameters", 187 | "schema": { 188 | "type": "string" 189 | } 190 | }, 191 | "401": { 192 | "description": "no device token supplied", 193 | "schema": { 194 | "type": "string" 195 | } 196 | }, 197 | "500": { 198 | "description": "unexpected error occurred", 199 | "schema": { 200 | "type": "string" 201 | } 202 | } 203 | } 204 | } 205 | } 206 | }, 207 | "definitions": { 208 | "http_device.deviceRegisterRequestDTO": { 209 | "type": "object", 210 | "properties": { 211 | "firmware_version": { 212 | "type": "string", 213 | "example": "1.0.0-1" 214 | }, 215 | "serial_number": { 216 | "type": "string", 217 | "example": "TEST-123" 218 | } 219 | } 220 | }, 221 | "http_device.deviceRegisterResponseDTO": { 222 | "type": "object", 223 | "properties": { 224 | "device": { 225 | "type": "object", 226 | "$ref": "#/definitions/registry.Device" 227 | }, 228 | "token": { 229 | "type": "string" 230 | } 231 | } 232 | }, 233 | "ingestion.DecimalMetricValue": { 234 | "type": "object", 235 | "properties": { 236 | "time": { 237 | "description": "Epoch timestamp in seconds", 238 | "type": "integer", 239 | "example": 1578859629 240 | }, 241 | "value": { 242 | "type": "number" 243 | } 244 | } 245 | }, 246 | "ingestion.DecimalMetricValueList": { 247 | "type": "array", 248 | "items": { 249 | "$ref": "#/definitions/ingestion.DecimalMetricValue" 250 | } 251 | }, 252 | "registry.Device": { 253 | "type": "object", 254 | "properties": { 255 | "firmware_version": { 256 | "type": "string", 257 | "example": "1.0.0-1" 258 | }, 259 | "id": { 260 | "type": "integer" 261 | }, 262 | "registration_date": { 263 | "type": "string", 264 | "format": "date-time", 265 | "example": "2017-07-21T17:32:28Z" 266 | }, 267 | "serial_number": { 268 | "type": "string", 269 | "example": "TEST-123" 270 | } 271 | } 272 | } 273 | }, 274 | "securityDefinitions": { 275 | "ApiKeyAuth": { 276 | "type": "apiKey", 277 | "name": "Authorization", 278 | "in": "header" 279 | } 280 | } 281 | }` 282 | 283 | type swaggerInfo struct { 284 | Version string 285 | Host string 286 | BasePath string 287 | Schemes []string 288 | Title string 289 | Description string 290 | } 291 | 292 | // SwaggerInfo holds exported Swagger Info so clients can modify it 293 | var SwaggerInfo = swaggerInfo{ 294 | Version: "1.0", 295 | Host: "", 296 | BasePath: "", 297 | Schemes: []string{"http"}, 298 | Title: "IOT Demo", 299 | Description: "Devices and Metrics API", 300 | } 301 | 302 | type s struct{} 303 | 304 | func (s *s) ReadDoc() string { 305 | sInfo := SwaggerInfo 306 | sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) 307 | 308 | t, err := template.New("swagger_info").Funcs(template.FuncMap{ 309 | "marshal": func(v interface{}) string { 310 | a, _ := json.Marshal(v) 311 | return string(a) 312 | }, 313 | }).Parse(doc) 314 | if err != nil { 315 | return doc 316 | } 317 | 318 | var tpl bytes.Buffer 319 | if err := t.Execute(&tpl, sInfo); err != nil { 320 | return doc 321 | } 322 | 323 | return tpl.String() 324 | } 325 | 326 | func init() { 327 | swag.Register(swag.Name, &s{}) 328 | } 329 | -------------------------------------------------------------------------------- /adapters/http/docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemes": [ 3 | "http" 4 | ], 5 | "swagger": "2.0", 6 | "info": { 7 | "description": "Devices and Metrics API", 8 | "title": "IOT Demo", 9 | "contact": { 10 | "name": "Yengas", 11 | "email": "yigitcan.ucum@trendyol.com" 12 | }, 13 | "license": { 14 | "name": "Apache 2.0", 15 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 16 | }, 17 | "version": "1.0" 18 | }, 19 | "paths": { 20 | "/_monitoring/health": { 21 | "get": { 22 | "description": "returns ok if the server is up", 23 | "consumes": [ 24 | "application/json" 25 | ], 26 | "produces": [ 27 | "application/json" 28 | ], 29 | "tags": [ 30 | "health" 31 | ], 32 | "summary": "get status of the server", 33 | "responses": { 34 | "200": { 35 | "description": "ok", 36 | "schema": { 37 | "type": "string" 38 | } 39 | } 40 | } 41 | } 42 | }, 43 | "/device": { 44 | "post": { 45 | "description": "register a new device with the given parameters", 46 | "consumes": [ 47 | "application/json" 48 | ], 49 | "produces": [ 50 | "application/json" 51 | ], 52 | "tags": [ 53 | "device" 54 | ], 55 | "summary": "register a new device", 56 | "parameters": [ 57 | { 58 | "description": "info of the device to register", 59 | "name": "device", 60 | "in": "body", 61 | "required": true, 62 | "schema": { 63 | "$ref": "#/definitions/http_device.deviceRegisterRequestDTO" 64 | } 65 | } 66 | ], 67 | "responses": { 68 | "201": { 69 | "description": "created new device", 70 | "schema": { 71 | "$ref": "#/definitions/http_device.deviceRegisterResponseDTO" 72 | } 73 | }, 74 | "400": { 75 | "description": "invalid request parameters", 76 | "schema": { 77 | "type": "string" 78 | } 79 | }, 80 | "500": { 81 | "description": "unexpected error occurred", 82 | "schema": { 83 | "type": "string" 84 | } 85 | } 86 | } 87 | } 88 | }, 89 | "/metric/temperature": { 90 | "get": { 91 | "description": "given a device and a starting date, returns all temperature metrics", 92 | "consumes": [ 93 | "application/json" 94 | ], 95 | "produces": [ 96 | "application/json" 97 | ], 98 | "tags": [ 99 | "metric" 100 | ], 101 | "summary": "query temperature metrics of devices", 102 | "parameters": [ 103 | { 104 | "type": "string", 105 | "description": "id of the device", 106 | "name": "deviceID", 107 | "in": "query", 108 | "required": true 109 | } 110 | ], 111 | "responses": { 112 | "200": { 113 | "description": "metrics matching the criteria", 114 | "schema": { 115 | "type": "array", 116 | "items": { 117 | "$ref": "#/definitions/ingestion.DecimalMetricValue" 118 | } 119 | } 120 | }, 121 | "404": { 122 | "description": "no metrics found", 123 | "schema": { 124 | "type": "string" 125 | } 126 | }, 127 | "500": { 128 | "description": "unexpected error occurred", 129 | "schema": { 130 | "type": "string" 131 | } 132 | } 133 | } 134 | }, 135 | "post": { 136 | "security": [ 137 | { 138 | "ApiKeyAuth": [] 139 | } 140 | ], 141 | "description": "inserts temperature metric data for the given device id", 142 | "consumes": [ 143 | "application/json" 144 | ], 145 | "produces": [ 146 | "application/json" 147 | ], 148 | "tags": [ 149 | "metric" 150 | ], 151 | "summary": "insert temperature metric data for devices", 152 | "parameters": [ 153 | { 154 | "description": "metrics to insert", 155 | "name": "metrics", 156 | "in": "body", 157 | "required": true, 158 | "schema": { 159 | "$ref": "#/definitions/ingestion.DecimalMetricValueList" 160 | } 161 | } 162 | ], 163 | "responses": { 164 | "201": { 165 | "description": "inserted the temperature metrics", 166 | "schema": { 167 | "type": "string" 168 | } 169 | }, 170 | "400": { 171 | "description": "invalid request parameters", 172 | "schema": { 173 | "type": "string" 174 | } 175 | }, 176 | "401": { 177 | "description": "no device token supplied", 178 | "schema": { 179 | "type": "string" 180 | } 181 | }, 182 | "500": { 183 | "description": "unexpected error occurred", 184 | "schema": { 185 | "type": "string" 186 | } 187 | } 188 | } 189 | } 190 | } 191 | }, 192 | "definitions": { 193 | "http_device.deviceRegisterRequestDTO": { 194 | "type": "object", 195 | "properties": { 196 | "firmware_version": { 197 | "type": "string", 198 | "example": "1.0.0-1" 199 | }, 200 | "serial_number": { 201 | "type": "string", 202 | "example": "TEST-123" 203 | } 204 | } 205 | }, 206 | "http_device.deviceRegisterResponseDTO": { 207 | "type": "object", 208 | "properties": { 209 | "device": { 210 | "type": "object", 211 | "$ref": "#/definitions/registry.Device" 212 | }, 213 | "token": { 214 | "type": "string" 215 | } 216 | } 217 | }, 218 | "ingestion.DecimalMetricValue": { 219 | "type": "object", 220 | "properties": { 221 | "time": { 222 | "description": "Epoch timestamp in seconds", 223 | "type": "integer", 224 | "example": 1578859629 225 | }, 226 | "value": { 227 | "type": "number" 228 | } 229 | } 230 | }, 231 | "ingestion.DecimalMetricValueList": { 232 | "type": "array", 233 | "items": { 234 | "$ref": "#/definitions/ingestion.DecimalMetricValue" 235 | } 236 | }, 237 | "registry.Device": { 238 | "type": "object", 239 | "properties": { 240 | "firmware_version": { 241 | "type": "string", 242 | "example": "1.0.0-1" 243 | }, 244 | "id": { 245 | "type": "integer" 246 | }, 247 | "registration_date": { 248 | "type": "string", 249 | "format": "date-time", 250 | "example": "2017-07-21T17:32:28Z" 251 | }, 252 | "serial_number": { 253 | "type": "string", 254 | "example": "TEST-123" 255 | } 256 | } 257 | } 258 | }, 259 | "securityDefinitions": { 260 | "ApiKeyAuth": { 261 | "type": "apiKey", 262 | "name": "Authorization", 263 | "in": "header" 264 | } 265 | } 266 | } -------------------------------------------------------------------------------- /adapters/http/docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | http_device.deviceRegisterRequestDTO: 3 | properties: 4 | firmware_version: 5 | example: 1.0.0-1 6 | type: string 7 | serial_number: 8 | example: TEST-123 9 | type: string 10 | type: object 11 | http_device.deviceRegisterResponseDTO: 12 | properties: 13 | device: 14 | $ref: '#/definitions/registry.Device' 15 | type: object 16 | token: 17 | type: string 18 | type: object 19 | ingestion.DecimalMetricValue: 20 | properties: 21 | time: 22 | description: Epoch timestamp in seconds 23 | example: 1578859629 24 | type: integer 25 | value: 26 | type: number 27 | type: object 28 | ingestion.DecimalMetricValueList: 29 | items: 30 | $ref: '#/definitions/ingestion.DecimalMetricValue' 31 | type: array 32 | registry.Device: 33 | properties: 34 | firmware_version: 35 | example: 1.0.0-1 36 | type: string 37 | id: 38 | type: integer 39 | registration_date: 40 | example: "2017-07-21T17:32:28Z" 41 | format: date-time 42 | type: string 43 | serial_number: 44 | example: TEST-123 45 | type: string 46 | type: object 47 | info: 48 | contact: 49 | email: yigitcan.ucum@trendyol.com 50 | name: Yengas 51 | description: Devices and Metrics API 52 | license: 53 | name: Apache 2.0 54 | url: http://www.apache.org/licenses/LICENSE-2.0.html 55 | title: IOT Demo 56 | version: "1.0" 57 | paths: 58 | /_monitoring/health: 59 | get: 60 | consumes: 61 | - application/json 62 | description: returns ok if the server is up 63 | produces: 64 | - application/json 65 | responses: 66 | "200": 67 | description: ok 68 | schema: 69 | type: string 70 | summary: get status of the server 71 | tags: 72 | - health 73 | /device: 74 | post: 75 | consumes: 76 | - application/json 77 | description: register a new device with the given parameters 78 | parameters: 79 | - description: info of the device to register 80 | in: body 81 | name: device 82 | required: true 83 | schema: 84 | $ref: '#/definitions/http_device.deviceRegisterRequestDTO' 85 | produces: 86 | - application/json 87 | responses: 88 | "201": 89 | description: created new device 90 | schema: 91 | $ref: '#/definitions/http_device.deviceRegisterResponseDTO' 92 | "400": 93 | description: invalid request parameters 94 | schema: 95 | type: string 96 | "500": 97 | description: unexpected error occurred 98 | schema: 99 | type: string 100 | summary: register a new device 101 | tags: 102 | - device 103 | /metric/temperature: 104 | get: 105 | consumes: 106 | - application/json 107 | description: given a device and a starting date, returns all temperature metrics 108 | parameters: 109 | - description: id of the device 110 | in: query 111 | name: deviceID 112 | required: true 113 | type: string 114 | produces: 115 | - application/json 116 | responses: 117 | "200": 118 | description: metrics matching the criteria 119 | schema: 120 | items: 121 | $ref: '#/definitions/ingestion.DecimalMetricValue' 122 | type: array 123 | "404": 124 | description: no metrics found 125 | schema: 126 | type: string 127 | "500": 128 | description: unexpected error occurred 129 | schema: 130 | type: string 131 | summary: query temperature metrics of devices 132 | tags: 133 | - metric 134 | post: 135 | consumes: 136 | - application/json 137 | description: inserts temperature metric data for the given device id 138 | parameters: 139 | - description: metrics to insert 140 | in: body 141 | name: metrics 142 | required: true 143 | schema: 144 | $ref: '#/definitions/ingestion.DecimalMetricValueList' 145 | produces: 146 | - application/json 147 | responses: 148 | "201": 149 | description: inserted the temperature metrics 150 | schema: 151 | type: string 152 | "400": 153 | description: invalid request parameters 154 | schema: 155 | type: string 156 | "401": 157 | description: no device token supplied 158 | schema: 159 | type: string 160 | "500": 161 | description: unexpected error occurred 162 | schema: 163 | type: string 164 | security: 165 | - ApiKeyAuth: [] 166 | summary: insert temperature metric data for devices 167 | tags: 168 | - metric 169 | schemes: 170 | - http 171 | securityDefinitions: 172 | ApiKeyAuth: 173 | in: header 174 | name: Authorization 175 | type: apiKey 176 | swagger: "2.0" 177 | -------------------------------------------------------------------------------- /adapters/http/health/health.go: -------------------------------------------------------------------------------- 1 | package http_health 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | // Health godoc 8 | // @Summary get status of the server 9 | // @Description returns ok if the server is up 10 | // @Tags health 11 | // @Accept json 12 | // @Produce json 13 | // @Success 200 {string} string "ok" 14 | // @Router /_monitoring/health [get] 15 | func makeGetHealthEndpoint() gin.HandlerFunc { 16 | return func(c *gin.Context) { 17 | c.String(200, "ok") 18 | } 19 | } 20 | 21 | type Handlers struct{} 22 | 23 | func (he Handlers) Register(engine *gin.Engine) { 24 | engine.GET("/_monitoring/health", makeGetHealthEndpoint()) 25 | } 26 | -------------------------------------------------------------------------------- /adapters/http/metrics/decimal.go: -------------------------------------------------------------------------------- 1 | package http_metrics 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "iot-demo/pkg/auth" 7 | add_metrics "iot-demo/pkg/metrics/add-metrics" 8 | "iot-demo/pkg/metrics/ingestion" 9 | query_metrics "iot-demo/pkg/metrics/query-metrics" 10 | "strconv" 11 | ) 12 | 13 | const ( 14 | invalidDecimalMetricData = "invalid decimal metric data" 15 | invalidDeviceID = "device id not valid" 16 | noMetricsFound = "no metric found for given device and dates" 17 | ) 18 | 19 | func decimalPostHandler(service *add_metrics.Service) gin.HandlerFunc { 20 | return func(c *gin.Context) { 21 | obj, exists := c.Get("auth_info") 22 | authInfo, ok := obj.(*auth.DeviceCredential) 23 | if !exists || !ok { 24 | c.String(401, "not authorized") 25 | return 26 | } 27 | 28 | var requestDTO []*ingestion.DecimalMetricValue 29 | if err := c.BindJSON(&requestDTO); err != nil { 30 | c.String(400, invalidDecimalMetricData) 31 | return 32 | } 33 | 34 | message, err := service.Add(authInfo.DeviceID, requestDTO) 35 | if err != nil { 36 | c.String(500, fmt.Errorf("insert error: %v", err).Error()) 37 | return 38 | } 39 | 40 | c.JSON(200, message) 41 | return 42 | } 43 | } 44 | 45 | func decimalQueryHandler(service *query_metrics.Service) gin.HandlerFunc { 46 | return func(c *gin.Context) { 47 | deviceIDSTR := c.Query("deviceID") 48 | deviceID, err := strconv.Atoi(deviceIDSTR) 49 | if err != nil { 50 | c.String(400, invalidDeviceID) 51 | return 52 | } 53 | 54 | metrics, err := service.Query(deviceID) 55 | if err != nil { 56 | c.String(500, err.Error()) 57 | return 58 | } 59 | 60 | if metrics == nil { 61 | c.String(404, noMetricsFound) 62 | return 63 | } 64 | 65 | c.JSON(200, metrics) 66 | return 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /adapters/http/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package http_metrics 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | add_metrics "iot-demo/pkg/metrics/add-metrics" 6 | query_metrics "iot-demo/pkg/metrics/query-metrics" 7 | ) 8 | 9 | type Handlers struct { 10 | AddMetricsService *add_metrics.Service 11 | QueryMetricsService *query_metrics.Service 12 | } 13 | 14 | func (drh Handlers) Register(engine *gin.Engine) { 15 | engine.POST("/metric/temperature", decimalPostHandler(drh.AddMetricsService)) 16 | engine.GET("/metric/temperature", decimalQueryHandler(drh.QueryMetricsService)) 17 | } 18 | -------------------------------------------------------------------------------- /adapters/http/metrics/temperature.go: -------------------------------------------------------------------------------- 1 | package http_metrics 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "iot-demo/pkg/metrics/ingestion" 6 | ) 7 | 8 | // DeviceRegistry godoc 9 | // @Summary insert temperature metric data for devices 10 | // @Description inserts temperature metric data for the given device id 11 | // @Security ApiKeyAuth 12 | // @Tags metric 13 | // @Accept json 14 | // @Produce json 15 | // @Param metrics body ingestion.DecimalMetricValueList true "metrics to insert" 16 | // @Success 201 {string} string "inserted the temperature metrics" 17 | // @Failure 400 {string} string "invalid request parameters" 18 | // @Failure 401 {string} string "no device token supplied" 19 | // @Failure 500 {string} string "unexpected error occurred" 20 | // @Router /metric/temperature [post] 21 | func noopTemperaturePostHandler(value ingestion.DecimalMetricValue) gin.HandlerFunc { return nil } 22 | 23 | // DeviceRegistry godoc 24 | // @Summary query temperature metrics of devices 25 | // @Description given a device and a starting date, returns all temperature metrics 26 | // @Tags metric 27 | // @Accept json 28 | // @Produce json 29 | // @Param deviceID query string true "id of the device" 30 | // @Success 200 {array} ingestion.DecimalMetricValue "metrics matching the criteria" 31 | // @Failure 404 {string} string "no metrics found" 32 | // @Failure 500 {string} string "unexpected error occurred" 33 | // @Router /metric/temperature [get] 34 | func noopTemperatureQueryHandler() gin.HandlerFunc { return nil } 35 | -------------------------------------------------------------------------------- /adapters/http/server.go: -------------------------------------------------------------------------------- 1 | package http_server 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "github.com/swaggo/gin-swagger" 8 | "github.com/swaggo/gin-swagger/swaggerFiles" 9 | http_device "iot-demo/adapters/http/device" 10 | "iot-demo/adapters/http/docs" 11 | http_health "iot-demo/adapters/http/health" 12 | http_metrics "iot-demo/adapters/http/metrics" 13 | add_device "iot-demo/pkg/device/add-device" 14 | add_metrics "iot-demo/pkg/metrics/add-metrics" 15 | query_metrics "iot-demo/pkg/metrics/query-metrics" 16 | "log" 17 | "net/http" 18 | ) 19 | 20 | //go:generate swag init --parseDependency -g server.go 21 | // @title IOT Demo 22 | // @version 1.0 23 | // @description Devices and Metrics API 24 | 25 | // @securityDefinitions.apikey ApiKeyAuth 26 | // @in header 27 | // @name Authorization 28 | 29 | // @license.name Apache 2.0 30 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 31 | 32 | // @contact.name Yengas 33 | // @contact.email yigitcan.ucum@trendyol.com 34 | 35 | // @Schemes http 36 | 37 | type httpHandleServer interface { 38 | ListenAndServe() error 39 | Shutdown(ctx context.Context) error 40 | } 41 | 42 | type Config struct { 43 | Host string 44 | DocumentationHost string 45 | DocumentationBasePath string 46 | Port int 47 | IsRelease bool 48 | } 49 | 50 | type HTTPServer struct { 51 | config Config 52 | server httpHandleServer 53 | } 54 | 55 | type InstrumentationHandler gin.HandlerFunc 56 | 57 | func NewHandlers( 58 | config Config, 59 | tokenParser DeviceTokenParser, 60 | addDevice *add_device.Service, 61 | addMetrics *add_metrics.Service, 62 | queryMetrics *query_metrics.Service, 63 | ) http.Handler { 64 | if config.IsRelease { 65 | gin.SetMode(gin.ReleaseMode) 66 | } 67 | 68 | engine := createGinEngine() 69 | 70 | engine.Use(deviceAuthParserHandler(tokenParser)) 71 | 72 | docs.SwaggerInfo.Host = config.DocumentationHost 73 | docs.SwaggerInfo.BasePath = config.DocumentationBasePath 74 | 75 | engine.GET("/swagger-ui/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 76 | engine.GET("", func(c *gin.Context) { 77 | c.Redirect(302, "/swagger-ui/index.html") 78 | c.Abort() 79 | }) 80 | 81 | http_health.Handlers{}.Register(engine) 82 | http_device.Handlers{Service: addDevice}.Register(engine) 83 | http_metrics.Handlers{AddMetricsService: addMetrics, QueryMetricsService: queryMetrics}.Register(engine) 84 | 85 | return engine 86 | } 87 | 88 | func createGinEngine() *gin.Engine { 89 | engine := gin.New() 90 | engine.Use(gin.Recovery()) 91 | engine.Use(gin.Logger()) 92 | return engine 93 | } 94 | 95 | func (httpServer *HTTPServer) Start() error { 96 | cfg := httpServer.config 97 | log.Printf("server is starting on %v:%v\n", cfg.Host, cfg.Port) 98 | return httpServer.server.ListenAndServe() 99 | } 100 | 101 | func (httpServer *HTTPServer) Stop(ctx context.Context) error { 102 | return httpServer.server.Shutdown(ctx) 103 | } 104 | 105 | func createServer(cfg Config, handler http.Handler) *http.Server { 106 | listenAddress := fmt.Sprintf("%v:%v", cfg.Host, cfg.Port) 107 | return &http.Server{Addr: listenAddress, Handler: handler} 108 | } 109 | 110 | func NewServer( 111 | config Config, 112 | handler http.Handler, 113 | ) *HTTPServer { 114 | return &HTTPServer{config: config, server: createServer(config, handler)} 115 | } 116 | -------------------------------------------------------------------------------- /adapters/jwt/device.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "errors" 5 | "github.com/dgrijalva/jwt-go" 6 | "iot-demo/pkg/auth" 7 | "strconv" 8 | ) 9 | 10 | type DeviceJWT AuthJWT 11 | 12 | func (dj DeviceJWT) Create(cred *auth.DeviceCredential) (auth.Token, error) { 13 | claims := jwt.MapClaims{ 14 | "id": strconv.Itoa(cred.DeviceID), 15 | } 16 | 17 | token, err := AuthJWT(dj).Sign(claims) 18 | if err != nil { 19 | return "", nil 20 | } 21 | 22 | return auth.Token(token), nil 23 | } 24 | 25 | func (dj DeviceJWT) Parse(authToken auth.Token) (*auth.DeviceCredential, error) { 26 | jwtToken, err := AuthJWT(dj).Parse(string(authToken)) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | if claims, ok := jwtToken.Claims.(jwt.MapClaims); ok && jwtToken.Valid { 32 | deviceIDSTR := claims["id"].(string) 33 | deviceID, err := strconv.Atoi(deviceIDSTR) 34 | if err != nil { 35 | return nil, errors.New("could not parse the claim") 36 | } 37 | return &auth.DeviceCredential{DeviceID: deviceID}, nil 38 | } 39 | 40 | return nil, errors.New("no claims or token is not valid") 41 | } 42 | -------------------------------------------------------------------------------- /adapters/jwt/jwt.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "fmt" 5 | "github.com/dgrijalva/jwt-go" 6 | ) 7 | 8 | type Config struct { 9 | Secret []byte 10 | } 11 | 12 | type AuthJWT struct { 13 | config Config 14 | } 15 | 16 | func (aj AuthJWT) Sign(claim jwt.MapClaims) (string, error) { 17 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim) 18 | str, err := token.SignedString(aj.config.Secret) 19 | if err != nil { 20 | return "", err 21 | } 22 | 23 | return str, nil 24 | } 25 | 26 | func (aj AuthJWT) Parse(str string) (*jwt.Token, error) { 27 | token, err := jwt.Parse(str, func(token *jwt.Token) (i interface{}, e error) { 28 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 29 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 30 | } 31 | 32 | return aj.config.Secret, nil 33 | }) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return token, nil 39 | } 40 | 41 | func NewJWT(config Config) AuthJWT { 42 | return AuthJWT{config: config} 43 | } 44 | -------------------------------------------------------------------------------- /adapters/storage/memory/ingestion.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "iot-demo/pkg/metrics/ingestion" 5 | ) 6 | 7 | // IngestionDecimal is non threadsafe repository implementation for ingestion package 8 | type IngestionDecimal struct { 9 | metrics map[int][]*ingestion.DecimalMetricValue 10 | } 11 | 12 | func (i *IngestionDecimal) Insert(deviceID int, metricsToInsert []*ingestion.DecimalMetricValue) error { 13 | for _, metric := range metricsToInsert { 14 | i.metrics[deviceID] = append(i.metrics[deviceID], metric) 15 | } 16 | return nil 17 | } 18 | 19 | func (i *IngestionDecimal) Query(deviceID int) ([]*ingestion.DecimalMetricValue, error) { 20 | if items, ok := i.metrics[deviceID]; ok { 21 | return items, nil 22 | } 23 | return nil, nil 24 | } 25 | 26 | func NewIngestion() *IngestionDecimal { 27 | metrics := make(map[int][]*ingestion.DecimalMetricValue) 28 | return &IngestionDecimal{metrics: metrics} 29 | } 30 | -------------------------------------------------------------------------------- /adapters/storage/memory/registry.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "iot-demo/pkg/device/registry" 5 | "time" 6 | ) 7 | 8 | // Registry is non threadsafe repository implementation for registry package 9 | type Registry struct { 10 | id int 11 | devices map[int]*registry.Device 12 | } 13 | 14 | func (r *Registry) Register(serialNumber string, firmwareVersion string, registrationDate time.Time) (*registry.Device, error) { 15 | r.id += 1 16 | device := registry.Device{ 17 | ID: r.id, 18 | SerialNumber: serialNumber, 19 | FirmwareVersion: firmwareVersion, 20 | RegistrationDate: registrationDate, 21 | } 22 | r.devices[r.id] = &device 23 | return &device, nil 24 | } 25 | 26 | func (r *Registry) Get(id int) (*registry.Device, bool) { 27 | device, ok := r.devices[id] 28 | return device, ok 29 | } 30 | 31 | func NewRegistry() *Registry { 32 | devices := make(map[int]*registry.Device) 33 | return &Registry{id: 0, devices: devices} 34 | } 35 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "iot-demo/adapters/config" 6 | http_server "iot-demo/adapters/http" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | func startServer(server *http_server.HTTPServer) { 16 | if err := server.Start(); err != nil && err != http.ErrServerClosed { 17 | log.Fatalf("could not start the application: %v", err.Error()) 18 | } 19 | } 20 | 21 | func createGracefulShutdownChannel() chan os.Signal { 22 | gracefulShutdown := make(chan os.Signal, 1) 23 | signal.Notify(gracefulShutdown, syscall.SIGTERM) 24 | signal.Notify(gracefulShutdown, syscall.SIGINT) 25 | return gracefulShutdown 26 | } 27 | 28 | func getConfigProfile() string { 29 | env := os.Getenv("PROFILE") 30 | if env != "" { 31 | return env 32 | } 33 | return "dev" 34 | } 35 | 36 | func main() { 37 | cfgSelection := config.Selection{ 38 | StaticConfigurationFilePath: "./resources/config.yaml", 39 | DynamicConfigurationFilePath: "./resources/config_dynamic.yaml", 40 | Profile: getConfigProfile(), 41 | } 42 | server, err := InitializeServer(cfgSelection) 43 | if err != nil { 44 | log.Fatalf("could not read `%v` config file: %v\n", cfgSelection, err) 45 | } 46 | gracefulShutdown := createGracefulShutdownChannel() 47 | // start the server and graceful shutdown 48 | go startServer(server) 49 | 50 | sig := <-gracefulShutdown 51 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 52 | defer cancel() 53 | 54 | log.Printf("caught sig: %+v, shutting down the application\n", sig) 55 | if err = server.Stop(ctx); err != nil { 56 | log.Fatalf("could not gracefully shutdown the application: %v", err.Error()) 57 | } 58 | log.Println("shutdown application gracefully.") 59 | } 60 | -------------------------------------------------------------------------------- /cmd/server/wire.go: -------------------------------------------------------------------------------- 1 | //+build wireinject 2 | 3 | package main 4 | 5 | import ( 6 | "github.com/google/wire" 7 | "iot-demo/adapters/config" 8 | http_server "iot-demo/adapters/http" 9 | "iot-demo/adapters/jwt" 10 | "iot-demo/adapters/storage/memory" 11 | add_device "iot-demo/pkg/device/add-device" 12 | "iot-demo/pkg/device/registry" 13 | add_metrics "iot-demo/pkg/metrics/add-metrics" 14 | "iot-demo/pkg/metrics/ingestion" 15 | query_metrics "iot-demo/pkg/metrics/query-metrics" 16 | ) 17 | 18 | func NewJWTConfig(cfg config.Static) jwt.Config { 19 | return jwt.Config{Secret: []byte(cfg.Auth.Secret)} 20 | } 21 | 22 | func NewDeviceJWT(authJWT jwt.AuthJWT) jwt.DeviceJWT { 23 | return jwt.DeviceJWT(authJWT) 24 | } 25 | 26 | func NewHTTPServerConfig(cfg config.Static) http_server.Config { 27 | return http_server.Config{ 28 | Host: cfg.Server.Host, 29 | DocumentationHost: cfg.Swagger.DocumentationHost, 30 | DocumentationBasePath: cfg.Swagger.DocumentationBasePath, 31 | Port: cfg.Server.Port, 32 | IsRelease: cfg.IsRelease, 33 | } 34 | } 35 | 36 | var ( 37 | configSet = wire.NewSet( 38 | config.ReadStatic, 39 | config.NewDynamicGetter, 40 | wire.Bind(new(add_metrics.ConfigGetter), new(*config.DynamicGetter)), 41 | ) 42 | jwtSet = wire.NewSet( 43 | NewJWTConfig, 44 | jwt.NewJWT, 45 | NewDeviceJWT, 46 | wire.Bind(new(http_server.DeviceTokenParser), new(jwt.DeviceJWT)), 47 | wire.Bind(new(add_device.Tokenizer), new(jwt.DeviceJWT)), 48 | ) 49 | storageSet = wire.NewSet( 50 | memory.NewIngestion, 51 | memory.NewRegistry, 52 | wire.Bind(new(registry.Repository), new(*memory.Registry)), 53 | wire.Bind(new(ingestion.DecimalRepository), new(*memory.IngestionDecimal)), 54 | ) 55 | httpSet = wire.NewSet( 56 | NewHTTPServerConfig, 57 | http_server.NewHandlers, 58 | http_server.NewServer, 59 | ) 60 | deviceSet = wire.NewSet( 61 | registry.NewService, 62 | wire.Bind(new(add_device.Registerer), new(*registry.Service)), 63 | ) 64 | ingestionSet = wire.NewSet( 65 | ingestion.NewDecimalService, 66 | wire.Bind(new(add_metrics.Inserter), new(*ingestion.DecimalService)), 67 | wire.Bind(new(query_metrics.Querier), new(*ingestion.DecimalService)), 68 | ) 69 | addDeviceSet = wire.NewSet(add_device.NewService) 70 | addMetricsSet = wire.NewSet(add_metrics.NewService) 71 | queryMetricsSet = wire.NewSet(query_metrics.NewService) 72 | ) 73 | 74 | func InitializeServer(cs config.Selection) (*http_server.HTTPServer, error) { 75 | wire.Build( 76 | configSet, 77 | jwtSet, 78 | deviceSet, 79 | ingestionSet, 80 | storageSet, 81 | addDeviceSet, 82 | addMetricsSet, 83 | queryMetricsSet, 84 | httpSet, 85 | ) 86 | return nil, nil 87 | } 88 | -------------------------------------------------------------------------------- /cmd/server/wire_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by Wire. DO NOT EDIT. 2 | 3 | //go:generate wire 4 | //+build !wireinject 5 | 6 | package main 7 | 8 | import ( 9 | "github.com/google/wire" 10 | "iot-demo/adapters/config" 11 | "iot-demo/adapters/http" 12 | "iot-demo/adapters/jwt" 13 | "iot-demo/adapters/storage/memory" 14 | "iot-demo/pkg/device/add-device" 15 | "iot-demo/pkg/device/registry" 16 | "iot-demo/pkg/metrics/add-metrics" 17 | "iot-demo/pkg/metrics/ingestion" 18 | "iot-demo/pkg/metrics/query-metrics" 19 | ) 20 | 21 | // Injectors from wire.go: 22 | 23 | func InitializeServer(cs config.Selection) (*http_server.HTTPServer, error) { 24 | static, err := config.ReadStatic(cs) 25 | if err != nil { 26 | return nil, err 27 | } 28 | http_serverConfig := NewHTTPServerConfig(static) 29 | jwtConfig := NewJWTConfig(static) 30 | authJWT := jwt.NewJWT(jwtConfig) 31 | deviceJWT := NewDeviceJWT(authJWT) 32 | memoryRegistry := memory.NewRegistry() 33 | service := registry.NewService(memoryRegistry) 34 | add_deviceService := add_device.NewService(service, deviceJWT) 35 | ingestionDecimal := memory.NewIngestion() 36 | decimalService := ingestion.NewDecimalService(ingestionDecimal) 37 | dynamicGetter, err := config.NewDynamicGetter(cs) 38 | if err != nil { 39 | return nil, err 40 | } 41 | add_metricsService := add_metrics.NewService(decimalService, dynamicGetter) 42 | query_metricsService := query_metrics.NewService(decimalService) 43 | handler := http_server.NewHandlers(http_serverConfig, deviceJWT, add_deviceService, add_metricsService, query_metricsService) 44 | httpServer := http_server.NewServer(http_serverConfig, handler) 45 | return httpServer, nil 46 | } 47 | 48 | // wire.go: 49 | 50 | func NewJWTConfig(cfg config.Static) jwt.Config { 51 | return jwt.Config{Secret: []byte(cfg.Auth.Secret)} 52 | } 53 | 54 | func NewDeviceJWT(authJWT jwt.AuthJWT) jwt.DeviceJWT { 55 | return jwt.DeviceJWT(authJWT) 56 | } 57 | 58 | func NewHTTPServerConfig(cfg config.Static) http_server.Config { 59 | return http_server.Config{ 60 | Host: cfg.Server.Host, 61 | DocumentationHost: cfg.Swagger.DocumentationHost, 62 | DocumentationBasePath: cfg.Swagger.DocumentationBasePath, 63 | Port: cfg.Server.Port, 64 | IsRelease: cfg.IsRelease, 65 | } 66 | } 67 | 68 | var ( 69 | configSet = wire.NewSet(config.ReadStatic, config.NewDynamicGetter, wire.Bind(new(add_metrics.ConfigGetter), new(*config.DynamicGetter))) 70 | jwtSet = wire.NewSet( 71 | NewJWTConfig, jwt.NewJWT, NewDeviceJWT, wire.Bind(new(http_server.DeviceTokenParser), new(jwt.DeviceJWT)), wire.Bind(new(add_device.Tokenizer), new(jwt.DeviceJWT)), 72 | ) 73 | storageSet = wire.NewSet(memory.NewIngestion, memory.NewRegistry, wire.Bind(new(registry.Repository), new(*memory.Registry)), wire.Bind(new(ingestion.DecimalRepository), new(*memory.IngestionDecimal))) 74 | httpSet = wire.NewSet( 75 | NewHTTPServerConfig, http_server.NewHandlers, http_server.NewServer, 76 | ) 77 | deviceSet = wire.NewSet(registry.NewService, wire.Bind(new(add_device.Registerer), new(*registry.Service))) 78 | ingestionSet = wire.NewSet(ingestion.NewDecimalService, wire.Bind(new(add_metrics.Inserter), new(*ingestion.DecimalService)), wire.Bind(new(query_metrics.Querier), new(*ingestion.DecimalService))) 79 | addDeviceSet = wire.NewSet(add_device.NewService) 80 | addMetricsSet = wire.NewSet(add_metrics.NewService) 81 | queryMetricsSet = wire.NewSet(query_metrics.NewService) 82 | ) 83 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module iot-demo 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 7 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 8 | github.com/fsnotify/fsnotify v1.4.7 9 | github.com/gin-gonic/gin v1.5.0 10 | github.com/golang/mock v1.4.0 11 | github.com/google/wire v0.4.0 12 | github.com/prometheus/common v0.4.0 13 | github.com/spf13/viper v1.6.2 14 | github.com/stretchr/testify v1.4.0 15 | github.com/swaggo/gin-swagger v1.2.0 16 | github.com/swaggo/swag v1.5.1 17 | gopkg.in/yaml.v2 v2.2.4 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 4 | github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4= 5 | github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 6 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 7 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 8 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 9 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 10 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 11 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= 12 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 13 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 14 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 15 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 16 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 17 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 18 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 19 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 20 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 21 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 22 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 27 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 28 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 29 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 30 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 31 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 32 | github.com/gin-contrib/gzip v0.0.1 h1:ezvKOL6jH+jlzdHNE4h9h8q8uMpDQjyl0NN0Jd7jozc= 33 | github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w= 34 | github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 35 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 36 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 37 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 38 | github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= 39 | github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= 40 | github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= 41 | github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= 42 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 43 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 44 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 45 | github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0= 46 | github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= 47 | github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= 48 | github.com/go-openapi/jsonreference v0.19.0 h1:BqWKpV1dFd+AuiKlgtddwVIFQsuMpxfBDBHGfM2yNpk= 49 | github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= 50 | github.com/go-openapi/spec v0.19.0 h1:A4SZ6IWh3lnjH0rG0Z5lkxazMGBECtrZcbyYQi+64k4= 51 | github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= 52 | github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880= 53 | github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= 54 | github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= 55 | github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= 56 | github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= 57 | github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= 58 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 59 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 60 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 61 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 62 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 63 | github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= 64 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 65 | github.com/golang/mock v1.4.0 h1:Rd1kQnQu0Hq3qvJppYSG0HtP+f5LPPUiDswTLiEegLg= 66 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 67 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 68 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 69 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 70 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 71 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 72 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 73 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 74 | github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k= 75 | github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 76 | github.com/google/wire v0.4.0 h1:kXcsA/rIGzJImVqPdhfnr6q0xsS9gU0515q1EPpJ9fE= 77 | github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= 78 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 79 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 80 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 81 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 82 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 83 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 84 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 85 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 86 | github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 87 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 88 | github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= 89 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 90 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 91 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 92 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 93 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 94 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 95 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 96 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 97 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 98 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 99 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 100 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 101 | github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= 102 | github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= 103 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 104 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 105 | github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic= 106 | github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 107 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 108 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 109 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 110 | github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= 111 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 112 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 113 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 114 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 115 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 116 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 117 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 118 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 119 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 120 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 121 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 122 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 123 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 124 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 125 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 126 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 127 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 128 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 129 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 130 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 131 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 132 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 133 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 134 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 135 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 136 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 137 | github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= 138 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 139 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 140 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 141 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 142 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 143 | github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= 144 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 145 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 146 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 147 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 148 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 149 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 150 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 151 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 152 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 153 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 154 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 155 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 156 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 157 | github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= 158 | github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= 159 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 160 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 161 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 162 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 163 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 164 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 165 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 166 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 167 | github.com/swaggo/gin-swagger v1.2.0 h1:YskZXEiv51fjOMTsXrOetAjrMDfFaXD79PEoQBOe2W0= 168 | github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI= 169 | github.com/swaggo/swag v1.5.1 h1:2Agm8I4K5qb00620mHq0VJ05/KT4FtmALPIcQR9lEZM= 170 | github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y= 171 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 172 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 173 | github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0= 174 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 175 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 176 | github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 177 | github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI= 178 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 179 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 180 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 181 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 182 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 183 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 184 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 185 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 186 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 187 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 188 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 189 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 190 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 191 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 192 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 193 | golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 194 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 195 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 196 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 197 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 198 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 199 | golang.org/x/net v0.0.0-20190611141213-3f473d35a33a h1:+KkCgOMgnKSgenxTBoiwkMqTiouMIy/3o8RLdmSbGoY= 200 | golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 201 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 202 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 203 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 204 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 205 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 206 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 207 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 208 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 209 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 210 | golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 211 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 212 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 213 | golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 214 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= 215 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 216 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 217 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 218 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 219 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 220 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 221 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 222 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 223 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 224 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 225 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 226 | golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 227 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 228 | golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 229 | golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b h1:/mJ+GKieZA6hFDQGdWZrjj4AXPl5ylY+5HusG80roy0= 230 | golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 231 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 232 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 233 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 234 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 235 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 236 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 237 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 238 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 239 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 240 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 241 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 242 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 243 | gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= 244 | gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 245 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 246 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 247 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 248 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 249 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 250 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 251 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 252 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 253 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 254 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 255 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 256 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 257 | -------------------------------------------------------------------------------- /mocks/add_metrics.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: iot-demo/pkg/metrics/add-metrics (interfaces: Inserter,ConfigGetter) 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | alert "iot-demo/pkg/metrics/alert" 10 | ingestion "iot-demo/pkg/metrics/ingestion" 11 | reflect "reflect" 12 | ) 13 | 14 | // MockInserter is a mock of Inserter interface 15 | type MockInserter struct { 16 | ctrl *gomock.Controller 17 | recorder *MockInserterMockRecorder 18 | } 19 | 20 | // MockInserterMockRecorder is the mock recorder for MockInserter 21 | type MockInserterMockRecorder struct { 22 | mock *MockInserter 23 | } 24 | 25 | // NewMockInserter creates a new mock instance 26 | func NewMockInserter(ctrl *gomock.Controller) *MockInserter { 27 | mock := &MockInserter{ctrl: ctrl} 28 | mock.recorder = &MockInserterMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use 33 | func (m *MockInserter) EXPECT() *MockInserterMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Insert mocks base method 38 | func (m *MockInserter) Insert(arg0 int, arg1 []*ingestion.DecimalMetricValue) error { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "Insert", arg0, arg1) 41 | ret0, _ := ret[0].(error) 42 | return ret0 43 | } 44 | 45 | // Insert indicates an expected call of Insert 46 | func (mr *MockInserterMockRecorder) Insert(arg0, arg1 interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockInserter)(nil).Insert), arg0, arg1) 49 | } 50 | 51 | // MockConfigGetter is a mock of ConfigGetter interface 52 | type MockConfigGetter struct { 53 | ctrl *gomock.Controller 54 | recorder *MockConfigGetterMockRecorder 55 | } 56 | 57 | // MockConfigGetterMockRecorder is the mock recorder for MockConfigGetter 58 | type MockConfigGetterMockRecorder struct { 59 | mock *MockConfigGetter 60 | } 61 | 62 | // NewMockConfigGetter creates a new mock instance 63 | func NewMockConfigGetter(ctrl *gomock.Controller) *MockConfigGetter { 64 | mock := &MockConfigGetter{ctrl: ctrl} 65 | mock.recorder = &MockConfigGetterMockRecorder{mock} 66 | return mock 67 | } 68 | 69 | // EXPECT returns an object that allows the caller to indicate expected use 70 | func (m *MockConfigGetter) EXPECT() *MockConfigGetterMockRecorder { 71 | return m.recorder 72 | } 73 | 74 | // GetThreshold mocks base method 75 | func (m *MockConfigGetter) GetThreshold() alert.Threshold { 76 | m.ctrl.T.Helper() 77 | ret := m.ctrl.Call(m, "GetThreshold") 78 | ret0, _ := ret[0].(alert.Threshold) 79 | return ret0 80 | } 81 | 82 | // GetThreshold indicates an expected call of GetThreshold 83 | func (mr *MockConfigGetterMockRecorder) GetThreshold() *gomock.Call { 84 | mr.mock.ctrl.T.Helper() 85 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetThreshold", reflect.TypeOf((*MockConfigGetter)(nil).GetThreshold)) 86 | } 87 | -------------------------------------------------------------------------------- /pkg/auth/device.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | type Token string 4 | 5 | type DeviceCredential struct { 6 | DeviceID int `json:"id"` 7 | } 8 | -------------------------------------------------------------------------------- /pkg/device/add-device/add_device.go: -------------------------------------------------------------------------------- 1 | package add_device 2 | 3 | import ( 4 | "iot-demo/pkg/auth" 5 | "iot-demo/pkg/device/registry" 6 | "time" 7 | ) 8 | 9 | type Registerer interface { 10 | Register(serialNumber string, firmwareVersion string, registrationDate time.Time) (*registry.Device, error) 11 | } 12 | 13 | type Tokenizer interface { 14 | Create(authInfo *auth.DeviceCredential) (auth.Token, error) 15 | } 16 | 17 | type Service struct { 18 | registerer Registerer 19 | tokenizer Tokenizer 20 | } 21 | 22 | func (s *Service) Register(serialNumber string, firmwareVersion string, registrationDate time.Time) (*registry.Device, auth.Token, error) { 23 | device, err := s.registerer.Register(serialNumber, firmwareVersion, registrationDate) 24 | if err != nil { 25 | return nil, "", err 26 | } 27 | 28 | authInfo := auth.DeviceCredential{DeviceID: device.ID} 29 | token, err := s.tokenizer.Create(&authInfo) 30 | if err != nil { 31 | return nil, "", err 32 | } 33 | 34 | return device, token, nil 35 | } 36 | 37 | func NewService(registerer Registerer, tokenizer Tokenizer) *Service { 38 | return &Service{ 39 | registerer: registerer, 40 | tokenizer: tokenizer, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/device/registry/device.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import "time" 4 | 5 | type Device struct { 6 | ID int `json:"id"` 7 | SerialNumber string `json:"serial_number" example:"TEST-123"` 8 | FirmwareVersion string `json:"firmware_version" example:"1.0.0-1"` 9 | RegistrationDate time.Time `json:"registration_date" format:"date-time" example:"2017-07-21T17:32:28Z"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/device/registry/register.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Repository interface { 8 | Register(serialNumber string, firmwareVersion string, registrationDate time.Time) (*Device, error) 9 | } 10 | 11 | type Service struct { 12 | repository Repository 13 | } 14 | 15 | func (s *Service) Register(serialNumber string, firmwareVersion string, registrationDate time.Time) (*Device, error) { 16 | return s.repository.Register(serialNumber, firmwareVersion, registrationDate) 17 | } 18 | 19 | func NewService(repository Repository) *Service { 20 | return &Service{repository: repository} 21 | } 22 | -------------------------------------------------------------------------------- /pkg/metrics/add-metrics/add_metrics.go: -------------------------------------------------------------------------------- 1 | package add_metrics 2 | 3 | import ( 4 | "iot-demo/pkg/metrics/alert" 5 | "iot-demo/pkg/metrics/ingestion" 6 | ) 7 | 8 | type ResponseToken string 9 | const ( 10 | Alert ResponseToken = "ALERT" 11 | Ok ResponseToken = "OK" 12 | ) 13 | 14 | type Inserter interface { 15 | Insert(deviceID int, metricsToInsert []*ingestion.DecimalMetricValue) error 16 | } 17 | 18 | type ConfigGetter interface { 19 | GetThreshold() alert.Threshold 20 | } 21 | 22 | type Service struct { 23 | inserter Inserter 24 | config ConfigGetter 25 | } 26 | 27 | func (s *Service) Add(deviceID int, metricsToInsert []*ingestion.DecimalMetricValue) (ResponseToken, error) { 28 | err := s.inserter.Insert(deviceID, metricsToInsert) 29 | if err != nil { 30 | return "", err 31 | } 32 | 33 | threshold := s.config.GetThreshold() 34 | return getMessage(threshold, metricsToInsert), nil 35 | } 36 | 37 | func getMessage(threshold alert.Threshold, metrics []*ingestion.DecimalMetricValue) ResponseToken { 38 | for _, metric := range metrics { 39 | if threshold.DoesMatch(metric.Value) { 40 | return Alert 41 | } 42 | } 43 | return Ok 44 | } 45 | 46 | func NewService(inserter Inserter, config ConfigGetter) *Service { 47 | return &Service{inserter: inserter, config: config} 48 | } 49 | -------------------------------------------------------------------------------- /pkg/metrics/add-metrics/add_metrics_test.go: -------------------------------------------------------------------------------- 1 | package add_metrics_test 2 | 3 | import ( 4 | "errors" 5 | "github.com/golang/mock/gomock" 6 | "github.com/stretchr/testify/assert" 7 | "iot-demo/mocks" 8 | add_metrics "iot-demo/pkg/metrics/add-metrics" 9 | "iot-demo/pkg/metrics/alert" 10 | "iot-demo/pkg/metrics/ingestion" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | //go:generate mockgen -package mocks -destination ../../../mocks/add_metrics.go iot-demo/pkg/metrics/add-metrics Inserter,ConfigGetter 16 | 17 | func TestAdd_Should_Fail_If_Insert_Fails(t *testing.T) { 18 | ctrl := gomock.NewController(t) 19 | defer ctrl.Finish() 20 | 21 | // Arrange 22 | deviceID, metricsToInsert := 52, []*ingestion.DecimalMetricValue(nil) 23 | expectedErr := errors.New("could not insert") 24 | 25 | inserter := mocks.NewMockInserter(ctrl) 26 | getter := mocks.NewMockConfigGetter(ctrl) 27 | 28 | addMetrics := add_metrics.NewService(inserter, getter) 29 | 30 | inserter. 31 | EXPECT(). 32 | Insert(deviceID, metricsToInsert). 33 | Return(expectedErr). 34 | Times(1) 35 | 36 | getter. 37 | EXPECT(). 38 | GetThreshold(). 39 | Times(0) 40 | 41 | // Act 42 | res, err := addMetrics.Add(deviceID, metricsToInsert) 43 | 44 | // Assert 45 | assert.Empty(t, res) 46 | assert.Equal(t, err, expectedErr) 47 | } 48 | 49 | func TestAdd_Should_Succeed_And_Return_Ok(t *testing.T) { 50 | ctrl := gomock.NewController(t) 51 | defer ctrl.Finish() 52 | 53 | // Arrange 54 | deviceID, metricsToInsert := 52, []*ingestion.DecimalMetricValue(nil) 55 | threshold := alert.Threshold{ 56 | Min: 0, 57 | Max: 0, 58 | } 59 | 60 | inserter := mocks.NewMockInserter(ctrl) 61 | getter := mocks.NewMockConfigGetter(ctrl) 62 | 63 | addMetrics := add_metrics.NewService(inserter, getter) 64 | 65 | inserter. 66 | EXPECT(). 67 | Insert(deviceID, metricsToInsert). 68 | Return(nil). 69 | Times(1) 70 | 71 | getter. 72 | EXPECT(). 73 | GetThreshold(). 74 | Return(threshold). 75 | Times(1) 76 | 77 | // Act 78 | res, err := addMetrics.Add(deviceID, metricsToInsert) 79 | 80 | // Assert 81 | assert.Nil(t, err) 82 | assert.Equal(t, res, add_metrics.Ok) 83 | } 84 | 85 | 86 | func TestAdd_Should_Alert_And_Return_Alert(t *testing.T) { 87 | ctrl := gomock.NewController(t) 88 | defer ctrl.Finish() 89 | 90 | // Arrange 91 | deviceID, metricsToInsert := 52, []*ingestion.DecimalMetricValue{{ 92 | Value: 5, 93 | Time: ingestion.Time(time.Now()), 94 | }} 95 | threshold := alert.Threshold{ 96 | Min: 1, 97 | Max: 10, 98 | } 99 | 100 | inserter := mocks.NewMockInserter(ctrl) 101 | getter := mocks.NewMockConfigGetter(ctrl) 102 | 103 | addMetrics := add_metrics.NewService(inserter, getter) 104 | 105 | inserter. 106 | EXPECT(). 107 | Insert(deviceID, metricsToInsert). 108 | Return(nil). 109 | Times(1) 110 | 111 | getter. 112 | EXPECT(). 113 | GetThreshold(). 114 | Return(threshold). 115 | Times(1) 116 | 117 | // Act 118 | res, err := addMetrics.Add(deviceID, metricsToInsert) 119 | 120 | // Assert 121 | assert.Nil(t, err) 122 | assert.Equal(t, res, add_metrics.Alert) 123 | } 124 | -------------------------------------------------------------------------------- /pkg/metrics/alert/alert.go: -------------------------------------------------------------------------------- 1 | package alert 2 | 3 | type Threshold struct { 4 | Min float64 5 | Max float64 6 | } 7 | 8 | func (t *Threshold) DoesMatch(value float64) bool { 9 | return value >= t.Min && value <= t.Max 10 | } 11 | -------------------------------------------------------------------------------- /pkg/metrics/alert/alert_test.go: -------------------------------------------------------------------------------- 1 | package alert 2 | 3 | import "testing" 4 | 5 | func TestThreshold_DoesMatch(t1 *testing.T) { 6 | type fields struct { 7 | Min float64 8 | Max float64 9 | } 10 | type args struct { 11 | value float64 12 | } 13 | tests := []struct { 14 | name string 15 | fields fields 16 | args args 17 | want bool 18 | }{ 19 | {"does match if min", fields{5, 10}, args{5}, true}, 20 | {"does match if max", fields{5, 10}, args{10}, true}, 21 | {"does match if between", fields{5, 10}, args{7}, true}, 22 | {"does not match if lesser than min", fields{5, 10}, args{4}, false}, 23 | {"does not match if bigger than max", fields{5, 10}, args{11}, false}, 24 | } 25 | for _, tt := range tests { 26 | t1.Run(tt.name, func(t1 *testing.T) { 27 | t := &Threshold{ 28 | Min: tt.fields.Min, 29 | Max: tt.fields.Max, 30 | } 31 | if got := t.DoesMatch(tt.args.value); got != tt.want { 32 | t1.Errorf("DoesMatch() = %v, want %v", got, tt.want) 33 | } 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/metrics/ingestion/metric.go: -------------------------------------------------------------------------------- 1 | package ingestion 2 | 3 | type DecimalMetricValueList []*DecimalMetricValue 4 | 5 | type DecimalMetricValue struct { 6 | Value float64 `json:"value"` 7 | // Epoch timestamp in seconds 8 | Time Time `json:"time" swaggertype:"primitive,integer" example:"1578859629"` 9 | } 10 | 11 | type DecimalRepository interface { 12 | Insert(deviceID int, metricsToInsert []*DecimalMetricValue) error 13 | Query(deviceID int) ([]*DecimalMetricValue, error) 14 | } 15 | 16 | type DecimalService struct { 17 | repository DecimalRepository 18 | } 19 | 20 | // insert metrics into the backing repository, notify all listeners 21 | func (ss *DecimalService) Insert(deviceID int, metricsToInsert []*DecimalMetricValue) error { 22 | err := ss.repository.Insert(deviceID, metricsToInsert) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | return nil 28 | } 29 | 30 | func (ss *DecimalService) Query(deviceID int) ([]*DecimalMetricValue, error) { 31 | return ss.repository.Query(deviceID) 32 | } 33 | 34 | func NewDecimalService(repository DecimalRepository) *DecimalService { 35 | return &DecimalService{repository: repository} 36 | } 37 | -------------------------------------------------------------------------------- /pkg/metrics/ingestion/time.go: -------------------------------------------------------------------------------- 1 | package ingestion 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | // Time defines a timestamp encoded as epoch seconds in JSON 9 | type Time time.Time 10 | 11 | // MarshalJSON is used to convert the timestamp to JSON 12 | func (t Time) MarshalJSON() ([]byte, error) { 13 | return []byte(strconv.FormatInt(time.Time(t).Unix(), 10)), nil 14 | } 15 | 16 | // UnmarshalJSON is used to convert the timestamp from JSON 17 | func (t *Time) UnmarshalJSON(s []byte) (err error) { 18 | r := string(s) 19 | q, err := strconv.ParseInt(r, 10, 64) 20 | if err != nil { 21 | return err 22 | } 23 | *(*time.Time)(t) = time.Unix(q, 0) 24 | return nil 25 | } 26 | 27 | // Unix returns t as a Unix time, the number of seconds elapsed 28 | // since January 1, 1970 UTC. The result does not depend on the 29 | // location associated with t. 30 | func (t Time) Unix() int64 { 31 | return time.Time(t).Unix() 32 | } 33 | 34 | // Time returns the JSON time as a time.Time instance in UTC 35 | func (t Time) Time() time.Time { 36 | return time.Time(t).UTC() 37 | } 38 | 39 | // String returns t as a formatted string 40 | func (t Time) String() string { 41 | return t.Time().String() 42 | } 43 | -------------------------------------------------------------------------------- /pkg/metrics/query-metrics/query_metrics.go: -------------------------------------------------------------------------------- 1 | package query_metrics 2 | 3 | import ( 4 | "iot-demo/pkg/metrics/ingestion" 5 | ) 6 | 7 | type Querier interface { 8 | Query(deviceID int) ([]*ingestion.DecimalMetricValue, error) 9 | } 10 | 11 | type Service struct { 12 | querier Querier 13 | } 14 | 15 | func (s *Service) Query(deviceID int) ([]*ingestion.DecimalMetricValue, error) { 16 | return s.querier.Query(deviceID) 17 | } 18 | 19 | func NewService(querier Querier) *Service { 20 | return &Service{querier: querier} 21 | } 22 | -------------------------------------------------------------------------------- /resources/config.yaml: -------------------------------------------------------------------------------- 1 | default: 2 | server: 3 | name: 'iot-demo' 4 | host: '0.0.0.0' 5 | port: 8080 6 | swagger: 7 | documentation_host: '' 8 | documentation_base_path: '/' 9 | auth: 10 | secret: 'iot-demo-secret' 11 | 12 | dev: {} 13 | 14 | stage: {} 15 | 16 | prod: {} 17 | -------------------------------------------------------------------------------- /resources/config_dynamic.yaml: -------------------------------------------------------------------------------- 1 | threshold: 2 | Min: 70 3 | Max: 100 4 | --------------------------------------------------------------------------------