├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CONFIGURATION.md ├── LICENSE ├── Makefile ├── README.md ├── SPEC.md ├── cmd └── wbt │ └── wbt.go ├── config ├── config.go ├── config_test.go └── example.toml ├── glide.lock ├── glide.yaml ├── jsonrpc ├── jsonrpc.go └── jsonrpc_test.go ├── server ├── builder.go ├── builder_test.go ├── log.go ├── proxy.go ├── server.go └── server_test.go ├── widebullet.go └── wlog ├── wlog.go └── wlog_test.go /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please read the CLA carefully before submitting your contribution to Mercari. 2 | Under any circumstances, by submitting your contribution, you are deemed to accept and agree to be bound by the terms and conditions of the CLA. 3 | 4 | https://www.mercari.com/cla/ 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor -------------------------------------------------------------------------------- /CONFIGURATION.md: -------------------------------------------------------------------------------- 1 | # Configuration for Widebullet 2 | 3 | A configuration file format for Widebullet is [TOML](https://github.com/toml-lang/toml). 4 | 5 | A configuration for Widebullet has some sections. A example is [here](config/example.toml). 6 | 7 | * [Global Section](#core-section) 8 | * [Endpoints Section](#endpoints-section) 9 | 10 | ## Global Section 11 | 12 | |name |type |description |default |note | 13 | |-------------------|------|--------------------------------------------|----------------|------------------------------------------------------------------| 14 | |Port |string|port number or unix socket path |29300 |e.g.)29300, unix:/tmp/wbt.sock
`-p` option can overwrite | 15 | |LogLevel |string|log-level |error | | 16 | |Timeout |int |timeout for proxying request |5 |unit is second | 17 | |MaxIdleConnsPerHost|int |maximum idle to keep per-host |100 | | 18 | |DisableCompression |bool |delete `Accept-Encoding: gzip` in header |false | | 19 | 20 | ## Endpoints Section 21 | 22 | |name |type |description |default|note| 23 | |---------------|--------------|-----------------------------------|-------|----| 24 | |Name |string |Endpoint name | | | 25 | |Ep |string |Endpoint URL | | | 26 | |ProxySetHeaders|array of array|Headers appended on proxying request| | | 27 | 28 | As a scheme, **http** and **https** are available for **Ep**. 29 | 30 | ``` 31 | Ep = "http://example.com" 32 | # or 33 | Ep = "https://example.com" 34 | ``` 35 | 36 | If a scheme is not specified, **http** is used. 37 | 38 | ``` 39 | Ep = "example.com" 40 | ``` 41 | 42 | * example.com 43 | 44 | # About API 45 | 46 | See [SPEC.md](SPEC.md) about details for APIs. 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Mercari, Inc. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGETS_NOVENDOR=$(shell glide novendor) 2 | 3 | all: wbt 4 | 5 | wbt: cmd/wbt/*.go server/*.go jsonrpc/*.go config/*.go wlog/*.go *.go 6 | go build cmd/wbt/wbt.go 7 | 8 | bundle: 9 | glide install 10 | 11 | check: 12 | go test $(TARGETS_NOVENDOR) 13 | 14 | fmt: 15 | @echo $(TARGETS_NOVENDOR) | xargs go fmt 16 | 17 | clean: 18 | rm -rf wbt 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Widebullet 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/mercari/widebullet)](https://goreportcard.com/report/github.com/mercari/widebullet) 4 | 5 | Widebullet is [JSON-RPC](http://www.jsonrpc.org/) base API gateway server. It implements [JSON-RPC batch](http://www.jsonrpc.org/specification#batch) endpoints with extended format for HTTP REST requests (see [SPEC](/SPEC.md)). For example, it receives one single JSON-RPC array which defines multiple HTTP requests and converts it into multiple concurrent HTTP requests. If you have multiple backend microservices and need to request them at same time for one transaction, Widebullet simplifies it. 6 | 7 | # Status 8 | 9 | Production ready. 10 | 11 | # Requirement 12 | 13 | Widebullet requires Go1.8 or later. 14 | 15 | # Installation 16 | 17 | Widebullet provides a executable named `wbt` to kick server. To install `wbt`, use `go get`, 18 | 19 | ``` 20 | $ go get -u github.com/mercari/widebullet/... 21 | ``` 22 | 23 | # Usage 24 | 25 | To run `wbt`, you must provide configuration path via `-c` option (See [CONFIGURATION.md](/CONFIGURATION.md)) about details and [`config/example.toml`](/config/example.toml) for example usage. 26 | 27 | ``` 28 | $ wbt -c config/example.toml 29 | ``` 30 | 31 | Use `-help` to see more options. 32 | 33 | 34 | # Configuration 35 | 36 | See [CONFIGURATION.md](/CONFIGURATION.md) about details. 37 | 38 | # Specification 39 | 40 | See [SPEC.md](/SPEC.md) about details. 41 | 42 | # Committers 43 | 44 | * Tatsuhiko Kubo [@cubicdaiya](https://github.com/cubicdaiya) 45 | 46 | # Contribution 47 | 48 | Please read the CLA below carefully before submitting your contribution. 49 | 50 | https://www.mercari.com/cla/ 51 | 52 | # License 53 | 54 | Copyright 2016 Mercari, Inc. 55 | 56 | Licensed under the MIT License. 57 | -------------------------------------------------------------------------------- /SPEC.md: -------------------------------------------------------------------------------- 1 | # Specification for Widebullet 2 | 3 | Widebullet is RESTful API gateway with JSON-RPC. It accepts a HTTP request based JSON-RPC. 4 | 5 | ## API 6 | 7 | Widebullet has the APIs below. 8 | 9 | * [POST /wbt](#post-wbt) 10 | * [GET /stat/go](#get-statgo) 11 | 12 | ### POST /wbt 13 | 14 | Accepts a HTTP request based JSON-RPC and proxies each converted HTTP request to a corresponding endpoint. 15 | And User-Agent and X-Forwarded-For in a request header are forwarded. 16 | 17 | The JSON below is a request-body example. 18 | 19 | ```json 20 | [ 21 | { 22 | "jsonrpc": "2.0", 23 | "ep": "ep-1", 24 | "method": "/user/get", 25 | "params": { 26 | "user_id": 1 27 | }, 28 | "id": "1" 29 | }, 30 | { 31 | "jsonrpc": "2.0", 32 | "ep": "ep-1", 33 | "http_method": "GET", 34 | "method": "/item/get", 35 | "params": { 36 | "item_id": 2 37 | }, 38 | "id": "2" 39 | }, 40 | { 41 | "jsonrpc": "2.0", 42 | "ep": "ep-2", 43 | "http_method": "POST", 44 | "method": "/item/update", 45 | "params": { 46 | "item_id": 2, 47 | "desc": "update" 48 | }, 49 | "id": "3" 50 | } 51 | ] 52 | ``` 53 | 54 | The definitions of parameters are below. 55 | 56 | |name |type |description |required|note | 57 | |----------------|------|-----------------------------------------|--------|----------------------------------| 58 | |jsonrpc |string|version number of JSON-RPC |o |fixed as 2.0 | 59 | |ep |string|endpoint name |o |selected in Endpoints Section | 60 | |http_method |string|method string for HTTP |o |HTTP method string. GET by default| 61 | |method |string|method string |o |URI | 62 | |params |object|parameters for method |o | | 63 | |id |string|ID string |o | | 64 | 65 | 66 | When Widebullet receives an invalid request(for example, malformed body is included), a status of response it returns is 400(Bad Request). 67 | 68 | ### GET /stat/go 69 | 70 | Returns a statictics for golang-runtime. See [golang-stats-api-handler](https://github.com/fukata/golang-stats-api-handler) about details. 71 | -------------------------------------------------------------------------------- /cmd/wbt/wbt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/mercari/widebullet" 14 | "github.com/mercari/widebullet/config" 15 | "github.com/mercari/widebullet/server" 16 | "github.com/mercari/widebullet/wlog" 17 | ) 18 | 19 | func main() { 20 | versionPrinted := flag.Bool("v", false, "print widebullet version") 21 | port := flag.String("p", "", "listening port number or socket path") 22 | configPath := flag.String("c", "", "configuration file path") 23 | flag.Parse() 24 | 25 | if *versionPrinted { 26 | wbt.PrintVersion() 27 | return 28 | } 29 | 30 | conf, err := config.Load(*configPath) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | // overwrite if port is specified by flags 36 | if *port != "" { 37 | conf.Port = *port 38 | } 39 | 40 | // set global configuration 41 | wbt.Config = conf 42 | wbt.AL = wlog.AccessLogger(conf.LogLevel) 43 | wbt.EL = wlog.ErrorLogger(conf.LogLevel) 44 | 45 | // Setup server 46 | mux := http.NewServeMux() 47 | server.RegisterHandlers(mux) 48 | server.SetupClient(&wbt.Config) 49 | 50 | srv := &http.Server{ 51 | Handler: mux, 52 | } 53 | 54 | go func() { 55 | wbt.EL.Out(wlog.Debug, "Start running server") 56 | if err := server.Run(srv, &wbt.Config); err != nil { 57 | wbt.EL.Out(wlog.Error, "Failed to run server: %s", err) 58 | } 59 | }() 60 | 61 | // Watch SIGTERM signal and then gracefully shutdown 62 | sigCh := make(chan os.Signal) 63 | signal.Notify(sigCh, syscall.SIGTERM) 64 | <-sigCh 65 | 66 | wbt.EL.Out(wlog.Debug, "Start to shutdown server") 67 | timeout := time.Duration(conf.ShutdownTimeout) * time.Second 68 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 69 | defer cancel() 70 | 71 | if err := srv.Shutdown(ctx); err != nil { 72 | wbt.EL.Out(wlog.Error, "Failed to shutdown server: %s", err) 73 | return 74 | } 75 | 76 | wbt.EL.Out(wlog.Debug, "Successfully shutdown server") 77 | } 78 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | 8 | "github.com/BurntSushi/toml" 9 | ) 10 | 11 | const ( 12 | DefaultPort = "29300" 13 | DefaultLogLevel = "error" 14 | DefaultTimeout = 5 15 | DefaultMaxIdleConnsPerHost = 100 16 | DefaultIdleConnTimeout = 30 17 | DefaultProxyReadTimeout = 60 18 | DefaultShutdownTimeout = 10 19 | ) 20 | 21 | type Config struct { 22 | Port string 23 | LogLevel string 24 | Timeout int 25 | MaxIdleConnsPerHost int 26 | DisableCompression bool 27 | IdleConnTimeout int 28 | ProxyReadTimeout int 29 | ShutdownTimeout int 30 | Endpoints []EndPoint 31 | } 32 | 33 | type EndPoint struct { 34 | Name string 35 | Ep string 36 | ProxySetHeaders [][]string 37 | } 38 | 39 | func LoadBytes(bytes []byte) (Config, error) { 40 | var config Config 41 | if err := toml.Unmarshal(bytes, &config); err != nil { 42 | return config, err 43 | } 44 | return config, nil 45 | } 46 | 47 | func Load(confPath string) (Config, error) { 48 | bytes, err := ioutil.ReadFile(confPath) 49 | if err != nil { 50 | return Config{}, err 51 | } 52 | 53 | config, err := LoadBytes(bytes) 54 | if err != nil { 55 | return Config{}, err 56 | } 57 | 58 | if config.Port == "" { 59 | config.Port = DefaultPort 60 | } 61 | 62 | if config.LogLevel == "" { 63 | config.LogLevel = DefaultLogLevel 64 | } 65 | 66 | if config.Timeout <= 0 { 67 | config.Timeout = DefaultTimeout 68 | } 69 | 70 | if config.MaxIdleConnsPerHost <= 0 { 71 | config.MaxIdleConnsPerHost = DefaultMaxIdleConnsPerHost 72 | } 73 | 74 | if config.IdleConnTimeout <= 0 { 75 | config.IdleConnTimeout = DefaultIdleConnTimeout 76 | } 77 | 78 | if config.ProxyReadTimeout <= 0 { 79 | config.ProxyReadTimeout = DefaultProxyReadTimeout 80 | } 81 | 82 | if config.ShutdownTimeout <= 0 { 83 | config.ShutdownTimeout = DefaultShutdownTimeout 84 | } 85 | 86 | if len(config.Endpoints) == 0 { 87 | return config, errors.New("empty Endpoints") 88 | } 89 | 90 | for _, ep := range config.Endpoints { 91 | if ep.Name == "" { 92 | return config, errors.New("empty Endpoint name") 93 | } 94 | if ep.Ep == "" { 95 | return config, errors.New("empty Endpoint URL") 96 | } 97 | } 98 | 99 | return config, nil 100 | } 101 | 102 | func FindEp(conf Config, name string) (EndPoint, error) { 103 | for _, ep := range conf.Endpoints { 104 | if ep.Name == name { 105 | return ep, nil 106 | } 107 | } 108 | 109 | return EndPoint{}, fmt.Errorf("ep:%s is not found", name) 110 | } 111 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLoadExampleToml(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | c, err := Load("./example.toml") 13 | assert.Nil(err) 14 | 15 | assert.Equal("29300", c.Port) 16 | assert.Equal("error", c.LogLevel) 17 | assert.Equal(5, c.Timeout) 18 | assert.Equal(100, c.MaxIdleConnsPerHost) 19 | assert.Equal(false, c.DisableCompression) 20 | assert.Equal(30, c.IdleConnTimeout) 21 | assert.Equal(60, c.ProxyReadTimeout) 22 | assert.Equal(15, c.ShutdownTimeout) 23 | 24 | eps := c.Endpoints 25 | assert.Equal(2, len(eps)) 26 | } 27 | 28 | func TestLoadGlobalConfig(t *testing.T) { 29 | assert := assert.New(t) 30 | 31 | configStr := ` 32 | Port = "12345" 33 | LogLevel = "debug" 34 | Timeout = 10 35 | MaxIdleConnsPerHost = 1000 36 | DisableCompression = true 37 | IdleConnTimeout = 90 38 | ProxyReadTimeout = 120 39 | ` 40 | 41 | c, err := LoadBytes([]byte(configStr)) 42 | assert.Nil(err) 43 | 44 | assert.Equal("12345", c.Port) 45 | assert.Equal("debug", c.LogLevel) 46 | assert.Equal(10, c.Timeout) 47 | assert.Equal(1000, c.MaxIdleConnsPerHost) 48 | assert.Equal(true, c.DisableCompression) 49 | assert.Equal(90, c.IdleConnTimeout) 50 | assert.Equal(120, c.ProxyReadTimeout) 51 | } 52 | 53 | func TestFindEp(t *testing.T) { 54 | assert := assert.New(t) 55 | 56 | c, err := Load("./example.toml") 57 | assert.Nil(err) 58 | 59 | ep, err := FindEp(c, "ep-1") 60 | assert.Nil(err) 61 | assert.Equal("ep-1", ep.Name) 62 | assert.Equal("127.0.0.1:30001", ep.Ep) 63 | assert.Equal("Host", ep.ProxySetHeaders[0][0]) 64 | assert.Equal("ep1.example.com", ep.ProxySetHeaders[0][1]) 65 | 66 | ep, err = FindEp(c, "ep-2") 67 | assert.Nil(err) 68 | assert.Equal("ep-2", ep.Name) 69 | assert.Equal("http://127.0.0.1:30002", ep.Ep) 70 | assert.Equal("Host", ep.ProxySetHeaders[0][0]) 71 | assert.Equal("ep2.example.com", ep.ProxySetHeaders[0][1]) 72 | 73 | _, err = FindEp(c, "ep-3") 74 | assert.NotNil(err) 75 | } 76 | -------------------------------------------------------------------------------- /config/example.toml: -------------------------------------------------------------------------------- 1 | Port = "29300" 2 | LogLevel = "error" 3 | Timeout = 5 4 | MaxIdleConnsPerHost = 100 5 | DisableCompression = false 6 | IdleConnTimeout = 30 7 | ProxyReadTimeout = 60 8 | ShutdownTimeout = 15 9 | 10 | [[Endpoints]] 11 | Name = "ep-1" 12 | Ep = "127.0.0.1:30001" 13 | ProxySetHeaders = [ 14 | ["Host", "ep1.example.com"], 15 | ] 16 | 17 | [[Endpoints]] 18 | Name = "ep-2" 19 | Ep = "http://127.0.0.1:30002" 20 | ProxySetHeaders = [ 21 | ["Host", "ep2.example.com"], 22 | ] 23 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 6b7cb7ceeba0a15b18c6b8ce8aaf82922eca3222d9ec494f49f2ad94aa6e2aac 2 | updated: 2017-02-24T11:50:32.724179827+09:00 3 | imports: 4 | - name: github.com/BurntSushi/toml 5 | version: bbd5bb678321a0d6e58f1099321dfa73391c1b6f 6 | - name: github.com/fujiwara/fluent-agent-hydra 7 | version: da59b0c40f6f3d8720dbfc1899a52c771e861ddd 8 | subpackages: 9 | - ltsv 10 | - name: github.com/fukata/golang-stats-api-handler 11 | version: e7ee1630fdb679c86ec8e3d0755e3576675fdb10 12 | - name: github.com/lestrrat/go-server-starter 13 | version: f9cb0b066498d26a90fd918fe265adbb0ea02bcf 14 | subpackages: 15 | - listener 16 | - name: github.com/stretchr/testify 17 | version: f390dcf405f7b83c997eac1b06768bb9f44dec18 18 | subpackages: 19 | - assert 20 | testImports: 21 | - name: github.com/davecgh/go-spew 22 | version: 2df174808ee097f90d259e432cc04442cf60be21 23 | subpackages: 24 | - spew 25 | - name: github.com/pmezard/go-difflib 26 | version: d8ed2627bdf02c080bf22230dbb337003b7aba2d 27 | subpackages: 28 | - difflib 29 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/mercari/widebullet 2 | import: 3 | - package: github.com/BurntSushi/toml 4 | version: v0.2.0 5 | - package: github.com/fujiwara/fluent-agent-hydra 6 | subpackages: 7 | - ltsv 8 | - package: github.com/fukata/golang-stats-api-handler 9 | version: e7ee1630fdb679c86ec8e3d0755e3576675fdb10 10 | - package: github.com/stretchr/testify 11 | version: v1.1.3 12 | - package: github.com/lestrrat/go-server-starter 13 | -------------------------------------------------------------------------------- /jsonrpc/jsonrpc.go: -------------------------------------------------------------------------------- 1 | package jsonrpc 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const Version = "2.0" 8 | 9 | const ( 10 | ParseError = -32700 11 | InvalidRequestError = -32600 12 | MethodNotFoundError = -32601 13 | InvalidParamsError = -32602 14 | InternalError = -32603 15 | ) 16 | 17 | type RequestParams map[string]interface{} 18 | 19 | type Request struct { 20 | Version string `json:"jsonrpc"` 21 | Method string `json:"method"` 22 | HttpMethod string `json:"http_method"` 23 | Params RequestParams `json:"params,omitempty"` 24 | ID string `json:"id"` 25 | // extention 26 | Ep string `json:"ep"` 27 | } 28 | 29 | type Response struct { 30 | Version string `json:"jsonrpc"` 31 | Result string `json:"result,omitempty"` 32 | Error *Error `json:"error,omitempty"` 33 | ID string `json:"id"` 34 | Time float64 `json:"time,omitempty"` 35 | } 36 | 37 | type Error struct { 38 | Code int `json:"code"` 39 | Message string `json:"message"` 40 | //Data interface{} `json:"data"` 41 | } 42 | 43 | func ValidateRequests(reqs *[]Request) error { 44 | idMap := make(map[string]bool, len(*reqs)) 45 | for _, r := range *reqs { 46 | if _, ok := idMap[r.ID]; ok { 47 | return fmt.Errorf("ID:%s is duplicated.", r.ID) 48 | } 49 | if err := validateRequest(&r); err != nil { 50 | return err 51 | } 52 | idMap[r.ID] = true 53 | } 54 | return nil 55 | } 56 | 57 | func validateRequest(r *Request) error { 58 | if r.Version != Version { 59 | return fmt.Errorf("malformed JSON-RPC version: %s", r.Version) 60 | } 61 | if r.Method == "" { 62 | return fmt.Errorf("empty method") 63 | } 64 | // empty method is treated as GET. 65 | if r.HttpMethod != "" && r.HttpMethod != "GET" && r.HttpMethod != "POST" { 66 | return fmt.Errorf("malformed HTTP method: %s", r.HttpMethod) 67 | } 68 | if r.ID == "" { 69 | return fmt.Errorf("empty id") 70 | } 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /jsonrpc/jsonrpc_test.go: -------------------------------------------------------------------------------- 1 | package jsonrpc 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestValidateRequest(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | params := make(map[string]interface{}) 13 | params["id"] = 1 14 | 15 | assert.Nil(validateRequest(&Request{ 16 | Version: "2.0", 17 | Method: "/resource/get", 18 | Params: params, 19 | ID: "1", 20 | })) 21 | } 22 | 23 | func TestValidateRequestVersion(t *testing.T) { 24 | assert := assert.New(t) 25 | 26 | params := make(map[string]interface{}) 27 | params["id"] = 1 28 | 29 | assert.NotNil(validateRequest(&Request{ 30 | Version: "1.0", 31 | Method: "/resource/get", 32 | Params: params, 33 | ID: "1", 34 | })) 35 | } 36 | 37 | func TestValidateRequestMethod(t *testing.T) { 38 | assert := assert.New(t) 39 | 40 | params := make(map[string]interface{}) 41 | params["id"] = 1 42 | 43 | assert.NotNil(validateRequest(&Request{ 44 | Version: "2.0", 45 | Method: "", 46 | Params: params, 47 | ID: "1", 48 | })) 49 | 50 | assert.Nil(validateRequest(&Request{ 51 | Version: "2.0", 52 | Method: "/reousrce/get", 53 | Params: params, 54 | ID: "1", 55 | })) 56 | } 57 | 58 | func TestValidateRequestHttpMethod(t *testing.T) { 59 | assert := assert.New(t) 60 | 61 | params := make(map[string]interface{}) 62 | params["id"] = 1 63 | 64 | assert.NotNil(validateRequest(&Request{ 65 | Version: "2.0", 66 | Method: "/resource/get", 67 | HttpMethod: "DELETE", 68 | Params: params, 69 | ID: "1", 70 | })) 71 | 72 | assert.Nil(validateRequest(&Request{ 73 | Version: "2.0", 74 | Method: "/resource/get", 75 | HttpMethod: "", 76 | Params: params, 77 | ID: "1", 78 | })) 79 | 80 | assert.Nil(validateRequest(&Request{ 81 | Version: "2.0", 82 | Method: "/resource/get", 83 | HttpMethod: "GET", 84 | Params: params, 85 | ID: "1", 86 | })) 87 | 88 | assert.Nil(validateRequest(&Request{ 89 | Version: "2.0", 90 | Method: "/resource/update", 91 | HttpMethod: "POST", 92 | Params: params, 93 | ID: "1", 94 | })) 95 | } 96 | 97 | func TestValidateRequestID(t *testing.T) { 98 | assert := assert.New(t) 99 | 100 | params := make(map[string]interface{}) 101 | params["id"] = 1 102 | 103 | assert.NotNil(validateRequest(&Request{ 104 | Version: "2.0", 105 | Method: "/resource/get", 106 | Params: params, 107 | ID: "", 108 | })) 109 | 110 | assert.Nil(validateRequest(&Request{ 111 | Version: "2.0", 112 | Method: "/reousrce/get", 113 | Params: params, 114 | ID: "1", 115 | })) 116 | } 117 | 118 | func TestValidateRequests(t *testing.T) { 119 | assert := assert.New(t) 120 | 121 | params := make(map[string]interface{}) 122 | params["id"] = 1 123 | 124 | reqs := make([]Request, 0) 125 | reqs = append(reqs, Request{ 126 | Version: "2.0", 127 | Method: "/resource/get1", 128 | Params: params, 129 | ID: "1", 130 | }) 131 | reqs = append(reqs, Request{ 132 | Version: "2.0", 133 | Method: "/resource/get2", 134 | Params: params, 135 | ID: "2", 136 | }) 137 | 138 | assert.Nil(ValidateRequests(&reqs)) 139 | } 140 | 141 | func TestValidateRequestsIDDup(t *testing.T) { 142 | assert := assert.New(t) 143 | 144 | params := make(map[string]interface{}) 145 | params["id"] = 1 146 | 147 | reqs := make([]Request, 0) 148 | reqs = append(reqs, Request{ 149 | Version: "2.0", 150 | Method: "/resource/get1", 151 | Params: params, 152 | ID: "1", 153 | }) 154 | reqs = append(reqs, Request{ 155 | Version: "2.0", 156 | Method: "/resource/get2", 157 | Params: params, 158 | ID: "1", 159 | }) 160 | 161 | assert.NotNil(ValidateRequests(&reqs)) 162 | } 163 | -------------------------------------------------------------------------------- /server/builder.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | 10 | "github.com/mercari/widebullet" 11 | "github.com/mercari/widebullet/config" 12 | "github.com/mercari/widebullet/jsonrpc" 13 | ) 14 | 15 | func buildRequestURI(ep, method, qs string) string { 16 | if strings.HasPrefix(ep, "http://") { 17 | return fmt.Sprintf("%s%s%s", ep, method, qs) 18 | } 19 | if strings.HasPrefix(ep, "https://") { 20 | return fmt.Sprintf("%s%s%s", ep, method, qs) 21 | } 22 | return fmt.Sprintf("http://%s%s%s", ep, method, qs) 23 | } 24 | 25 | func buildURLEncodedString(params jsonrpc.RequestParams, method string) (string, error) { 26 | values := url.Values{} 27 | for k, v := range params { 28 | switch v.(type) { 29 | case string: 30 | values.Set(k, v.(string)) 31 | case json.Number: 32 | values.Set(k, fmt.Sprintf("%s", v)) 33 | default: 34 | return "", fmt.Errorf("wbt supports only string and number") 35 | } 36 | } 37 | 38 | if method == "POST" { 39 | return values.Encode(), nil 40 | } 41 | 42 | return fmt.Sprintf("?%s", values.Encode()), nil 43 | } 44 | 45 | func buildJsonRpcResponse(body, id string, time float64) jsonrpc.Response { 46 | return jsonrpc.Response{ 47 | Version: jsonrpc.Version, 48 | Result: body, 49 | ID: id, 50 | Time: time, 51 | } 52 | } 53 | 54 | func buildJsonRpcErrorResponse(code int, msg, id string, time float64) jsonrpc.Response { 55 | jsonRpcError := &jsonrpc.Error{ 56 | Code: code, 57 | Message: msg, 58 | } 59 | 60 | return jsonrpc.Response{ 61 | Version: jsonrpc.Version, 62 | Error: jsonRpcError, 63 | ID: id, 64 | Time: time, 65 | } 66 | } 67 | 68 | func buildHttpError2JsonRpcErrorResponse(resp *http.Response, id string, time float64) jsonrpc.Response { 69 | switch resp.StatusCode { 70 | case http.StatusNotFound: 71 | return buildJsonRpcErrorResponse(jsonrpc.MethodNotFoundError, resp.Status, id, time) 72 | } 73 | return buildJsonRpcErrorResponse(jsonrpc.InternalError, resp.Status, id, time) 74 | } 75 | 76 | func buildHttpRequest(reqj *jsonrpc.Request, forwardHeaders *http.Header) (*http.Request, error) { 77 | var reqh *http.Request 78 | 79 | ep, err := config.FindEp(wbt.Config, reqj.Ep) 80 | if err != nil { 81 | return reqh, err 82 | } 83 | 84 | es, err := buildURLEncodedString(reqj.Params, reqj.HttpMethod) 85 | if err != nil { 86 | return reqh, err 87 | } 88 | 89 | switch reqj.HttpMethod { 90 | case "POST": 91 | uri := buildRequestURI(ep.Ep, reqj.Method, "") 92 | reqh, err = http.NewRequest("POST", uri, strings.NewReader(es)) 93 | if err != nil { 94 | return reqh, err 95 | } 96 | reqh.Header.Set("Content-Type", "application/x-www-form-urlencoded") 97 | default: 98 | uri := buildRequestURI(ep.Ep, reqj.Method, es) 99 | reqh, err = http.NewRequest("GET", uri, nil) 100 | if err != nil { 101 | return reqh, err 102 | } 103 | } 104 | 105 | ua := forwardHeaders.Get("User-Agent") 106 | if ua == "" { 107 | reqh.Header.Set("User-Agent", wbt.ServerHeader()) 108 | } else { 109 | reqh.Header.Set("User-Agent", ua) 110 | } 111 | 112 | xForwardedFor := forwardHeaders.Get("X-Forwarded-For") 113 | if xForwardedFor != "" { 114 | reqh.Header.Set("X-Forwarded-For", xForwardedFor) 115 | } 116 | 117 | for _, headers := range ep.ProxySetHeaders { 118 | if len(headers) < 2 { 119 | continue 120 | } 121 | key := headers[0] 122 | value := strings.Join(headers[1:], ",") 123 | if key == "Host" { 124 | reqh.Host = value 125 | } else { 126 | reqh.Header.Set(key, value) 127 | } 128 | } 129 | 130 | return reqh, nil 131 | } 132 | -------------------------------------------------------------------------------- /server/builder_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/mercari/widebullet" 11 | "github.com/mercari/widebullet/config" 12 | "github.com/mercari/widebullet/jsonrpc" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestBuildRequestURI(t *testing.T) { 17 | assert := assert.New(t) 18 | 19 | assert.Equal("http://127.0.0.1:30001/resource/get?id=1", 20 | buildRequestURI("127.0.0.1:30001", "/resource/get", "?id=1")) 21 | assert.Equal("http://127.0.0.1:30001/resource/get?id=1", 22 | buildRequestURI("http://127.0.0.1:30001", "/resource/get", "?id=1")) 23 | } 24 | 25 | func TestBuildHttpRequest(t *testing.T) { 26 | assert := assert.New(t) 27 | 28 | payload := `[ 29 | {"jsonrpc": "2.0", "ep": "ep-1", "method": "/user/get", "params": { "user_id": 1 }, "id": "1"}, 30 | {"jsonrpc": "2.0", "ep": "ep-1", "http_method": "GET", "method": "/item/get", "params": { "item_id": 2 }, "id": "2"}, 31 | {"jsonrpc": "2.0", "ep": "ep-2", "http_method": "POST", "method": "/item/update", "params": { "item_id": 2, "desc": "update" }, "id": "3"} 32 | ] 33 | ` 34 | 35 | var ( 36 | reqjs []jsonrpc.Request 37 | reqhs []http.Request 38 | buf bytes.Buffer 39 | ) 40 | decoder := json.NewDecoder(strings.NewReader(payload)) 41 | decoder.UseNumber() 42 | err := decoder.Decode(&reqjs) 43 | assert.Nil(err) 44 | 45 | wbt.Config, err = config.Load("../config/example.toml") 46 | assert.Nil(err) 47 | 48 | headers := make(http.Header) 49 | headers.Add("X-Forwarded-For", "127.0.0.1") 50 | for _, reqj := range reqjs { 51 | reqh, err := buildHttpRequest(&reqj, &headers) 52 | assert.Nil(err) 53 | assert.Equal(wbt.ServerHeader(), reqh.Header.Get("User-Agent")) 54 | assert.Equal("127.0.0.1", reqh.Header.Get("X-Forwarded-For")) 55 | reqhs = append(reqhs, *reqh) 56 | } 57 | 58 | assert.Equal("", reqhs[0].Header.Get("Content-Type")) 59 | assert.Equal("ep1.example.com", reqhs[0].Host) 60 | assert.Equal("127.0.0.1:30001", reqhs[0].URL.Host) 61 | assert.Equal("/user/get", reqhs[0].URL.Path) 62 | assert.Equal("user_id=1", reqhs[0].URL.RawQuery) 63 | 64 | assert.Equal("", reqhs[1].Header.Get("Content-Type")) 65 | assert.Equal("ep1.example.com", reqhs[1].Host) 66 | assert.Equal("127.0.0.1:30001", reqhs[1].URL.Host) 67 | assert.Equal("/item/get", reqhs[1].URL.Path) 68 | assert.Equal("item_id=2", reqhs[1].URL.RawQuery) 69 | 70 | buf.ReadFrom(reqhs[2].Body) 71 | assert.Equal("application/x-www-form-urlencoded", reqhs[2].Header.Get("Content-Type")) 72 | assert.Equal("ep2.example.com", reqhs[2].Host) 73 | assert.Equal("127.0.0.1:30002", reqhs[2].URL.Host) 74 | assert.Equal("/item/update", reqhs[2].URL.Path) 75 | assert.Equal("desc=update&item_id=2", buf.String()) 76 | } 77 | -------------------------------------------------------------------------------- /server/log.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "math" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | "github.com/fujiwara/fluent-agent-hydra/ltsv" 11 | "github.com/mercari/widebullet" 12 | "github.com/mercari/widebullet/jsonrpc" 13 | "github.com/mercari/widebullet/wlog" 14 | ) 15 | 16 | func accessLog(r *http.Request, rr *[]jsonrpc.Request, stime time.Time, status int) { 17 | etime := time.Now() 18 | ptime := math.Floor(etime.Sub(stime).Seconds()*1000) / 1000 19 | records := make(map[string]interface{}) 20 | 21 | records["time"] = time.Now().Local().Format("2006/01/02 15:04:05 MST") 22 | records["addr"] = r.RemoteAddr 23 | records["status"] = status 24 | records["ptime"] = ptime 25 | records["length"] = r.ContentLength 26 | if wbt.Config.LogLevel == "debug" { 27 | records["headers"] = r.Header 28 | records["body"] = *rr 29 | } 30 | buf := &bytes.Buffer{} 31 | encoder := ltsv.NewEncoder(buf) 32 | encoder.Encode(records) 33 | wbt.AL.Out(wlog.Info, strings.TrimRight(buf.String(), "\n")) 34 | } 35 | 36 | func errorLog(level wlog.LogLevel, msg string, args ...interface{}) { 37 | wbt.EL.Out(level, msg, args...) 38 | } 39 | -------------------------------------------------------------------------------- /server/proxy.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "sync" 7 | "time" 8 | 9 | "github.com/mercari/widebullet/jsonrpc" 10 | "github.com/mercari/widebullet/wlog" 11 | ) 12 | 13 | func sendHttpRequest(wg *sync.WaitGroup, reqj jsonrpc.Request, forwardHeaders *http.Header, respj *jsonrpc.Response) { 14 | defer wg.Done() 15 | reqh, err := buildHttpRequest(&reqj, forwardHeaders) 16 | if err != nil { 17 | *respj = buildJsonRpcErrorResponse(jsonrpc.InternalError, err.Error(), reqj.ID, 0) 18 | errorLog(wlog.Error, err.Error()) 19 | return 20 | } 21 | start := time.Now() 22 | resp, err := HttpClient.Do(reqh) 23 | end := time.Now() 24 | ptime := (end.Sub(start)).Seconds() 25 | if err != nil { 26 | *respj = buildJsonRpcErrorResponse(jsonrpc.InternalError, err.Error(), reqj.ID, ptime) 27 | errorLog(wlog.Error, err.Error()) 28 | return 29 | } 30 | defer resp.Body.Close() 31 | if resp.StatusCode != 200 { 32 | *respj = buildHttpError2JsonRpcErrorResponse(resp, reqj.ID, ptime) 33 | errorLog(wlog.Error, "%#v is failed: %s", reqj, resp.Status) 34 | return 35 | } 36 | body, err := ioutil.ReadAll(resp.Body) 37 | if err != nil { 38 | *respj = buildJsonRpcErrorResponse(jsonrpc.InternalError, err.Error(), reqj.ID, ptime) 39 | errorLog(wlog.Error, err.Error()) 40 | return 41 | } 42 | *respj = buildJsonRpcResponse(string(body), reqj.ID, ptime) 43 | } 44 | 45 | func jsonRpc2Http(reqs *[]jsonrpc.Request, forwardHeaders *http.Header) ([]jsonrpc.Response, error) { 46 | wg := new(sync.WaitGroup) 47 | resps := make([]jsonrpc.Response, len(*reqs)) 48 | // send requests to endpoint conccurrently 49 | for i, reqj := range *reqs { 50 | wg.Add(1) 51 | go sendHttpRequest(wg, reqj, forwardHeaders, &resps[i]) 52 | } 53 | 54 | wg.Wait() 55 | 56 | return resps, nil 57 | } 58 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "os" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | statsGo "github.com/fukata/golang-stats-api-handler" 14 | "github.com/lestrrat/go-server-starter/listener" 15 | "github.com/mercari/widebullet" 16 | "github.com/mercari/widebullet/config" 17 | "github.com/mercari/widebullet/jsonrpc" 18 | "github.com/mercari/widebullet/wlog" 19 | ) 20 | 21 | var ( 22 | HttpClient http.Client 23 | ) 24 | 25 | // RegisterHandlers sets handler to serve. 26 | func RegisterHandlers(mux *http.ServeMux) { 27 | mux.HandleFunc("/wbt", wideBulletHandler) 28 | 29 | statsGo.PrettyPrintEnabled() 30 | mux.HandleFunc("/stat/go", statsGo.Handler) 31 | } 32 | 33 | // SetupClient setups http.Client (which is globally used in this package) 34 | // with given config. 35 | func SetupClient(config *config.Config) { 36 | HttpClient = http.Client{ 37 | Timeout: time.Duration(config.Timeout) * time.Second, 38 | Transport: &http.Transport{ 39 | DialContext: (&net.Dialer{ 40 | Timeout: 30 * time.Second, 41 | KeepAlive: 30 * time.Second, 42 | }).DialContext, 43 | MaxIdleConnsPerHost: wbt.Config.MaxIdleConnsPerHost, 44 | DisableCompression: wbt.Config.DisableCompression, 45 | IdleConnTimeout: time.Duration(config.IdleConnTimeout) * time.Second, 46 | ResponseHeaderTimeout: time.Duration(config.ProxyReadTimeout) * time.Second, 47 | }, 48 | } 49 | } 50 | 51 | // Run starts the given server. By default, it tries to accept 52 | // requests from `go-server-starter`. If not then check config.Port 53 | // value. If nothing to listen, then returns error. 54 | func Run(server *http.Server, config *config.Config) error { 55 | 56 | // If ServerStarterEnv is found, then use listerner from 57 | // `go-server-starter`. Even if it fails, not terminate here 58 | // but use the given config.Port 59 | if v := os.Getenv(listener.ServerStarterEnvVarName); len(v) != 0 { 60 | listeners, err := listener.ListenAll() 61 | if err != nil { 62 | errorLog(wlog.Error, "Failed to get listeners from go-server-starter: %s", err) 63 | } else { 64 | if len(listeners) == 0 { 65 | errorLog(wlog.Error, "No listener to listen is found") 66 | } else { 67 | errorLog(wlog.Debug, "Start accepting request: %s", listeners[0].Addr()) 68 | return server.Serve(listeners[0]) 69 | } 70 | } 71 | } 72 | 73 | port := config.Port 74 | if len(port) == 0 { 75 | return fmt.Errorf("no port to listen") 76 | } 77 | 78 | // Listen TCP Port 79 | if _, err := strconv.Atoi(port); err == nil { 80 | errorLog(wlog.Debug, "Start listening: %s", port) 81 | server.Addr = ":" + port 82 | return server.ListenAndServe() 83 | } 84 | 85 | // Listen UNIX Socket 86 | if strings.HasPrefix(port, "unix:/") { 87 | sockPath := port[5:] 88 | fi, err := os.Lstat(sockPath) 89 | if err == nil && (fi.Mode()&os.ModeSocket) == os.ModeSocket { 90 | err := os.Remove(sockPath) 91 | if err != nil { 92 | return fmt.Errorf("failed to remove socket: %s", sockPath) 93 | } 94 | } 95 | 96 | l, err := net.Listen("unix", sockPath) 97 | if err != nil { 98 | return fmt.Errorf("failed to listen socket %q: %s", sockPath, err) 99 | } 100 | 101 | errorLog(wlog.Debug, "Start accepting request: %s", l.Addr()) 102 | return server.Serve(l) 103 | } 104 | 105 | return fmt.Errorf("failed to listen port: %s", port) 106 | } 107 | 108 | func sendTextResponse(w http.ResponseWriter, result string, code int) { 109 | w.Header().Set("Content-Type", "text/plain") 110 | w.Header().Set("Server", wbt.ServerHeader()) 111 | w.WriteHeader(code) 112 | fmt.Fprint(w, result) 113 | } 114 | 115 | func sendJsonResponse(w http.ResponseWriter, result string) { 116 | w.Header().Set("Content-Type", "application/json") 117 | w.Header().Set("Server", wbt.ServerHeader()) 118 | fmt.Fprint(w, result) 119 | } 120 | 121 | func wideBulletHandler(w http.ResponseWriter, r *http.Request) { 122 | var reqs []jsonrpc.Request 123 | 124 | stime := time.Now() 125 | 126 | if r.Method != "POST" { 127 | accessLog(r, &reqs, stime, http.StatusBadRequest) 128 | sendTextResponse(w, "method must be POST", http.StatusBadRequest) 129 | return 130 | } 131 | 132 | decoder := json.NewDecoder(r.Body) 133 | decoder.UseNumber() 134 | if err := decoder.Decode(&reqs); err != nil { 135 | accessLog(r, &reqs, stime, http.StatusBadRequest) 136 | sendTextResponse(w, "request is malformed", http.StatusBadRequest) 137 | return 138 | } 139 | 140 | if err := jsonrpc.ValidateRequests(&reqs); err != nil { 141 | accessLog(r, &reqs, stime, http.StatusBadRequest) 142 | errorLog(wlog.Error, err.Error()) 143 | sendTextResponse(w, err.Error(), http.StatusBadRequest) 144 | return 145 | } 146 | 147 | resps, err := jsonRpc2Http(&reqs, &r.Header) 148 | if err != nil { 149 | accessLog(r, &reqs, stime, http.StatusBadGateway) 150 | errorLog(wlog.Error, err.Error()) 151 | sendTextResponse(w, err.Error(), http.StatusBadGateway) 152 | return 153 | } 154 | 155 | bytes, err := json.Marshal(&resps) 156 | if err != nil { 157 | accessLog(r, &reqs, stime, http.StatusInternalServerError) 158 | errorLog(wlog.Error, err.Error()) 159 | sendTextResponse(w, err.Error(), http.StatusInternalServerError) 160 | return 161 | } 162 | 163 | sendJsonResponse(w, string(bytes)) 164 | 165 | accessLog(r, &reqs, stime, http.StatusOK) 166 | } 167 | -------------------------------------------------------------------------------- /server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/mercari/widebullet" 13 | "github.com/mercari/widebullet/config" 14 | "github.com/mercari/widebullet/jsonrpc" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func responseHandler(w http.ResponseWriter, r *http.Request) { 19 | fmt.Fprint(w, fmt.Sprintf("%s", r.URL)) 20 | } 21 | 22 | func TestWideBulletHandler(t *testing.T) { 23 | assert := assert.New(t) 24 | 25 | muxGET := http.NewServeMux() 26 | muxGET.HandleFunc("/user/get", responseHandler) 27 | muxGET.HandleFunc("/item/get", responseHandler) 28 | go http.ListenAndServe(":30001", muxGET) 29 | muxPOST := http.NewServeMux() 30 | muxPOST.HandleFunc("/item/update", responseHandler) 31 | go http.ListenAndServe(":30002", muxPOST) 32 | 33 | var err error 34 | wbt.Config, err = config.Load("../config/example.toml") 35 | assert.Nil(err) 36 | 37 | ts := httptest.NewServer(http.HandlerFunc(wideBulletHandler)) 38 | defer ts.Close() 39 | 40 | payload := `[ 41 | {"jsonrpc": "2.0", "ep": "ep-1", "method": "/user/get", "params": { "user_id": 1 }, "id": "1"}, 42 | {"jsonrpc": "2.0", "ep": "ep-1", "http_method": "GET", "method": "/item/get", "params": { "item_id": 2 }, "id": "2"}, 43 | {"jsonrpc": "2.0", "ep": "ep-2", "http_method": "POST", "method": "/item/update", "params": { "item_id": 2, "desc": "update" }, "id": "3"} 44 | ] 45 | ` 46 | res, err := http.Post(ts.URL, "application/json", strings.NewReader(payload)) 47 | assert.Nil(err) 48 | defer res.Body.Close() 49 | 50 | assert.Equal(200, res.StatusCode) 51 | body, err := ioutil.ReadAll(res.Body) 52 | assert.Nil(err) 53 | 54 | var resj []jsonrpc.Response 55 | err = json.Unmarshal(body, &resj) 56 | assert.Nil(err) 57 | 58 | for _, res := range resj { 59 | assert.Equal("2.0", res.Version) 60 | assert.Nil(res.Error) 61 | switch res.ID { 62 | case "1": 63 | assert.Equal("/user/get?user_id=1", res.Result) 64 | case "2": 65 | assert.Equal("/item/get?item_id=2", res.Result) 66 | case "3": 67 | assert.Equal("/item/update", res.Result) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /widebullet.go: -------------------------------------------------------------------------------- 1 | package wbt 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | 7 | "github.com/mercari/widebullet/config" 8 | "github.com/mercari/widebullet/wlog" 9 | ) 10 | 11 | const ( 12 | Version = "0.3.1" 13 | ) 14 | 15 | var ( 16 | Config config.Config 17 | AL wlog.Logger 18 | EL wlog.Logger 19 | ) 20 | 21 | func ServerHeader() string { 22 | return fmt.Sprintf("WideBullet %s", Version) 23 | } 24 | 25 | func PrintVersion() { 26 | fmt.Printf(`wbt %s 27 | Compiler: %s %s 28 | Copyright (C) 2016 Mercari, Inc. 29 | `, 30 | Version, 31 | runtime.Compiler, 32 | runtime.Version()) 33 | } 34 | -------------------------------------------------------------------------------- /wlog/wlog.go: -------------------------------------------------------------------------------- 1 | package wlog 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | type Redirector int 9 | type LogLevel int 10 | 11 | const ( 12 | Stdout Redirector = iota 13 | Stderr 14 | ) 15 | 16 | const ( 17 | Debug LogLevel = iota 18 | Info 19 | Notice 20 | Warn 21 | Error 22 | Crit 23 | ) 24 | 25 | type Logger struct { 26 | Rdr Redirector 27 | Level LogLevel 28 | } 29 | 30 | func New(r Redirector, level string) Logger { 31 | return Logger{ 32 | Rdr: r, 33 | Level: string2Level(level), 34 | } 35 | } 36 | 37 | func AccessLogger(level string) Logger { 38 | return New(Stdout, level) 39 | } 40 | 41 | func ErrorLogger(level string) Logger { 42 | return New(Stderr, level) 43 | } 44 | 45 | func (l *Logger) Out(level LogLevel, msg string, args ...interface{}) { 46 | if l.Rdr == Stderr { 47 | if level >= l.Level { 48 | fmt.Fprintln(os.Stderr, fmt.Sprintf(fmtMessage(level, msg), args...)) 49 | } 50 | } else { 51 | fmt.Fprintln(os.Stdout, fmt.Sprintf(fmtMessage(level, msg), args...)) 52 | } 53 | } 54 | 55 | func string2Level(s string) LogLevel { 56 | var result LogLevel 57 | switch s { 58 | case "debug": 59 | result = Debug 60 | case "notice": 61 | result = Notice 62 | case "warn": 63 | result = Warn 64 | case "error": 65 | result = Error 66 | case "crit": 67 | result = Crit 68 | case "info": 69 | fallthrough 70 | default: 71 | result = Info 72 | } 73 | return result 74 | } 75 | 76 | func level2String(level LogLevel) string { 77 | var result string 78 | switch level { 79 | case Debug: 80 | result = "debug" 81 | case Notice: 82 | result = "notice" 83 | case Warn: 84 | result = "warn" 85 | case Error: 86 | result = "error" 87 | case Crit: 88 | result = "crit" 89 | case Info: 90 | fallthrough 91 | default: 92 | result = "info" 93 | } 94 | return result 95 | } 96 | 97 | func fmtMessage(level LogLevel, msg string) string { 98 | return fmt.Sprintf("level:%s\t%s", level2String(level), msg) 99 | } 100 | -------------------------------------------------------------------------------- /wlog/wlog_test.go: -------------------------------------------------------------------------------- 1 | package wlog 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNewAccessLogger(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | al := AccessLogger("debug") 13 | assert.Equal(Stdout, al.Rdr) 14 | assert.Equal(Debug, al.Level) 15 | 16 | al = AccessLogger("info") 17 | assert.Equal(Stdout, al.Rdr) 18 | assert.Equal(Info, al.Level) 19 | 20 | al = AccessLogger("notice") 21 | assert.Equal(Stdout, al.Rdr) 22 | assert.Equal(Notice, al.Level) 23 | 24 | al = AccessLogger("warn") 25 | assert.Equal(Stdout, al.Rdr) 26 | assert.Equal(Warn, al.Level) 27 | 28 | al = AccessLogger("error") 29 | assert.Equal(Stdout, al.Rdr) 30 | assert.Equal(Error, al.Level) 31 | 32 | al = AccessLogger("crit") 33 | assert.Equal(Stdout, al.Rdr) 34 | assert.Equal(Crit, al.Level) 35 | } 36 | 37 | func TestNewErrorLogger(t *testing.T) { 38 | assert := assert.New(t) 39 | 40 | el := ErrorLogger("debug") 41 | assert.Equal(Stderr, el.Rdr) 42 | assert.Equal(Debug, el.Level) 43 | 44 | el = ErrorLogger("info") 45 | assert.Equal(Stderr, el.Rdr) 46 | assert.Equal(Info, el.Level) 47 | 48 | el = ErrorLogger("notice") 49 | assert.Equal(Stderr, el.Rdr) 50 | assert.Equal(Notice, el.Level) 51 | 52 | el = ErrorLogger("warn") 53 | assert.Equal(Stderr, el.Rdr) 54 | assert.Equal(Warn, el.Level) 55 | 56 | el = ErrorLogger("error") 57 | assert.Equal(Stderr, el.Rdr) 58 | assert.Equal(Error, el.Level) 59 | 60 | el = ErrorLogger("crit") 61 | assert.Equal(Stderr, el.Rdr) 62 | assert.Equal(Crit, el.Level) 63 | } 64 | 65 | func TestLevel2String(t *testing.T) { 66 | assert := assert.New(t) 67 | 68 | assert.Equal("debug", level2String(Debug)) 69 | assert.Equal("info", level2String(Info)) 70 | assert.Equal("notice", level2String(Notice)) 71 | assert.Equal("warn", level2String(Warn)) 72 | assert.Equal("error", level2String(Error)) 73 | assert.Equal("crit", level2String(Crit)) 74 | } 75 | --------------------------------------------------------------------------------