0 | 0 |
package main
|
1 | 1 |
|
2 | 2 |
import (
|
|
3 |
"bytes"
|
3 | 4 |
"encoding/json"
|
4 | 5 |
"flag"
|
5 | 6 |
"fmt"
|
6 | 7 |
"io"
|
7 | 8 |
"io/ioutil"
|
8 | |
stdlog "log"
|
9 | 9 |
"net/http"
|
10 | 10 |
"net/url"
|
11 | 11 |
"os"
|
|
16 | 16 |
|
17 | 17 |
"github.com/gorilla/mux"
|
18 | 18 |
"github.com/hashicorp/consul/api"
|
19 | |
"github.com/opentracing/opentracing-go"
|
|
19 |
stdopentracing "github.com/opentracing/opentracing-go"
|
20 | 20 |
"golang.org/x/net/context"
|
21 | 21 |
|
22 | 22 |
"github.com/go-kit/kit/endpoint"
|
23 | |
"github.com/go-kit/kit/examples/addsvc/client/grpc"
|
24 | |
"github.com/go-kit/kit/examples/addsvc/server"
|
25 | |
"github.com/go-kit/kit/loadbalancer"
|
26 | |
"github.com/go-kit/kit/loadbalancer/consul"
|
|
23 |
"github.com/go-kit/kit/examples/addsvc"
|
|
24 |
addsvcgrpcclient "github.com/go-kit/kit/examples/addsvc/client/grpc"
|
27 | 25 |
"github.com/go-kit/kit/log"
|
|
26 |
"github.com/go-kit/kit/sd"
|
|
27 |
consulsd "github.com/go-kit/kit/sd/consul"
|
|
28 |
"github.com/go-kit/kit/sd/lb"
|
28 | 29 |
httptransport "github.com/go-kit/kit/transport/http"
|
|
30 |
"google.golang.org/grpc"
|
29 | 31 |
)
|
30 | 32 |
|
31 | 33 |
func main() {
|
|
37 | 39 |
)
|
38 | 40 |
flag.Parse()
|
39 | 41 |
|
40 | |
// Log domain
|
41 | |
logger := log.NewLogfmtLogger(os.Stderr)
|
42 | |
logger = log.NewContext(logger).With("ts", log.DefaultTimestampUTC).With("caller", log.DefaultCaller)
|
43 | |
stdlog.SetFlags(0) // flags are handled by Go kit's logger
|
44 | |
stdlog.SetOutput(log.NewStdlibAdapter(logger)) // redirect anything using stdlib log to us
|
|
42 |
// Logging domain.
|
|
43 |
var logger log.Logger
|
|
44 |
{
|
|
45 |
logger = log.NewLogfmtLogger(os.Stderr)
|
|
46 |
logger = log.NewContext(logger).With("ts", log.DefaultTimestampUTC)
|
|
47 |
logger = log.NewContext(logger).With("caller", log.DefaultCaller)
|
|
48 |
}
|
45 | 49 |
|
46 | 50 |
// Service discovery domain. In this example we use Consul.
|
47 | |
consulConfig := api.DefaultConfig()
|
48 | |
if len(*consulAddr) > 0 {
|
49 | |
consulConfig.Address = *consulAddr
|
50 | |
}
|
51 | |
consulClient, err := api.NewClient(consulConfig)
|
52 | |
if err != nil {
|
53 | |
logger.Log("err", err)
|
54 | |
os.Exit(1)
|
55 | |
}
|
56 | |
discoveryClient := consul.NewClient(consulClient)
|
57 | |
|
58 | |
// Context domain.
|
|
51 |
var client consulsd.Client
|
|
52 |
{
|
|
53 |
consulConfig := api.DefaultConfig()
|
|
54 |
if len(*consulAddr) > 0 {
|
|
55 |
consulConfig.Address = *consulAddr
|
|
56 |
}
|
|
57 |
consulClient, err := api.NewClient(consulConfig)
|
|
58 |
if err != nil {
|
|
59 |
logger.Log("err", err)
|
|
60 |
os.Exit(1)
|
|
61 |
}
|
|
62 |
client = consulsd.NewClient(consulClient)
|
|
63 |
}
|
|
64 |
|
|
65 |
// Transport domain.
|
|
66 |
tracer := stdopentracing.GlobalTracer() // no-op
|
59 | 67 |
ctx := context.Background()
|
60 | |
|
61 | |
// Set up our routes.
|
62 | |
//
|
63 | |
// Each Consul service name maps to multiple instances of that service. We
|
64 | |
// connect to each instance according to its pre-determined transport: in this
|
65 | |
// case, we choose to access addsvc via its gRPC client, and stringsvc over
|
66 | |
// plain transport/http (it has no client package).
|
67 | |
//
|
68 | |
// Each service instance implements multiple methods, and we want to map each
|
69 | |
// method to a unique path on the API gateway. So, we define that path and its
|
70 | |
// corresponding factory function, which takes an instance string and returns an
|
71 | |
// endpoint.Endpoint for the specific method.
|
72 | |
//
|
73 | |
// Finally, we mount that path + endpoint handler into the router.
|
74 | 68 |
r := mux.NewRouter()
|
75 | |
for consulName, methods := range map[string][]struct {
|
76 | |
path string
|
77 | |
factory loadbalancer.Factory
|
78 | |
}{
|
79 | |
"addsvc": {
|
80 | |
{path: "/api/addsvc/concat", factory: grpc.MakeConcatEndpointFactory(opentracing.GlobalTracer(), nil)},
|
81 | |
{path: "/api/addsvc/sum", factory: grpc.MakeSumEndpointFactory(opentracing.GlobalTracer(), nil)},
|
82 | |
},
|
83 | |
"stringsvc": {
|
84 | |
{path: "/api/stringsvc/uppercase", factory: httpFactory(ctx, "GET", "uppercase/")},
|
85 | |
{path: "/api/stringsvc/concat", factory: httpFactory(ctx, "GET", "concat/")},
|
86 | |
},
|
87 | |
} {
|
88 | |
for _, method := range methods {
|
89 | |
publisher, err := consul.NewPublisher(discoveryClient, method.factory, logger, consulName)
|
90 | |
if err != nil {
|
91 | |
logger.Log("service", consulName, "path", method.path, "err", err)
|
92 | |
continue
|
93 | |
}
|
94 | |
lb := loadbalancer.NewRoundRobin(publisher)
|
95 | |
e := loadbalancer.Retry(*retryMax, *retryTimeout, lb)
|
96 | |
h := makeHandler(ctx, e, logger)
|
97 | |
r.HandleFunc(method.path, h)
|
98 | |
}
|
99 | |
}
|
100 | |
|
101 | |
// Mechanical stuff.
|
|
69 |
|
|
70 |
// Now we begin installing the routes. Each route corresponds to a single
|
|
71 |
// method: sum, concat, uppercase, and count.
|
|
72 |
|
|
73 |
// addsvc routes.
|
|
74 |
{
|
|
75 |
// Each method gets constructed with a factory. Factories take an
|
|
76 |
// instance string, and return a specific endpoint. In the factory we
|
|
77 |
// dial the instance string we get from Consul, and then leverage an
|
|
78 |
// addsvc client package to construct a complete service. We can then
|
|
79 |
// leverage the addsvc.Make{Sum,Concat}Endpoint constructors to convert
|
|
80 |
// the complete service to specific endpoint.
|
|
81 |
|
|
82 |
var (
|
|
83 |
tags = []string{}
|
|
84 |
passingOnly = true
|
|
85 |
endpoints = addsvc.Endpoints{}
|
|
86 |
)
|
|
87 |
{
|
|
88 |
factory := addsvcFactory(addsvc.MakeSumEndpoint, tracer, logger)
|
|
89 |
subscriber := consulsd.NewSubscriber(client, factory, logger, "addsvc", tags, passingOnly)
|
|
90 |
balancer := lb.NewRoundRobin(subscriber)
|
|
91 |
retry := lb.Retry(*retryMax, *retryTimeout, balancer)
|
|
92 |
endpoints.SumEndpoint = retry
|
|
93 |
}
|
|
94 |
{
|
|
95 |
factory := addsvcFactory(addsvc.MakeConcatEndpoint, tracer, logger)
|
|
96 |
subscriber := consulsd.NewSubscriber(client, factory, logger, "addsvc", tags, passingOnly)
|
|
97 |
balancer := lb.NewRoundRobin(subscriber)
|
|
98 |
retry := lb.Retry(*retryMax, *retryTimeout, balancer)
|
|
99 |
endpoints.ConcatEndpoint = retry
|
|
100 |
}
|
|
101 |
|
|
102 |
// Here we leverage the fact that addsvc comes with a constructor for an
|
|
103 |
// HTTP handler, and just install it under a particular path prefix in
|
|
104 |
// our router.
|
|
105 |
|
|
106 |
r.PathPrefix("addsvc/").Handler(addsvc.MakeHTTPHandler(ctx, endpoints, tracer, logger))
|
|
107 |
}
|
|
108 |
|
|
109 |
// stringsvc routes.
|
|
110 |
{
|
|
111 |
// addsvc had lots of nice importable Go packages we could leverage.
|
|
112 |
// With stringsvc we are not so fortunate, it just has some endpoints
|
|
113 |
// that we assume will exist. So we have to write that logic here. This
|
|
114 |
// is by design, so you can see two totally different methods of
|
|
115 |
// proxying to a remote service.
|
|
116 |
|
|
117 |
var (
|
|
118 |
tags = []string{}
|
|
119 |
passingOnly = true
|
|
120 |
uppercase endpoint.Endpoint
|
|
121 |
count endpoint.Endpoint
|
|
122 |
)
|
|
123 |
{
|
|
124 |
factory := stringsvcFactory(ctx, "GET", "/uppercase")
|
|
125 |
subscriber := consulsd.NewSubscriber(client, factory, logger, "stringsvc", tags, passingOnly)
|
|
126 |
balancer := lb.NewRoundRobin(subscriber)
|
|
127 |
retry := lb.Retry(*retryMax, *retryTimeout, balancer)
|
|
128 |
uppercase = retry
|
|
129 |
}
|
|
130 |
{
|
|
131 |
factory := stringsvcFactory(ctx, "GET", "/count")
|
|
132 |
subscriber := consulsd.NewSubscriber(client, factory, logger, "stringsvc", tags, passingOnly)
|
|
133 |
balancer := lb.NewRoundRobin(subscriber)
|
|
134 |
retry := lb.Retry(*retryMax, *retryTimeout, balancer)
|
|
135 |
count = retry
|
|
136 |
}
|
|
137 |
|
|
138 |
// We can use the transport/http.Server to act as our handler, all we
|
|
139 |
// have to do provide it with the encode and decode functions for our
|
|
140 |
// stringsvc methods.
|
|
141 |
|
|
142 |
r.Handle("/stringsvc/uppercase", httptransport.NewServer(ctx, uppercase, decodeUppercaseRequest, encodeJSONResponse))
|
|
143 |
r.Handle("/stringsvc/count", httptransport.NewServer(ctx, count, decodeCountRequest, encodeJSONResponse))
|
|
144 |
}
|
|
145 |
|
|
146 |
// Interrupt handler.
|
102 | 147 |
errc := make(chan error)
|
103 | 148 |
go func() {
|
104 | |
errc <- interrupt()
|
|
149 |
c := make(chan os.Signal)
|
|
150 |
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
|
151 |
errc <- fmt.Errorf("%s", <-c)
|
105 | 152 |
}()
|
|
153 |
|
|
154 |
// HTTP transport.
|
106 | 155 |
go func() {
|
107 | |
logger.Log("transport", "http", "addr", *httpAddr)
|
|
156 |
logger.Log("transport", "HTTP", "addr", *httpAddr)
|
108 | 157 |
errc <- http.ListenAndServe(*httpAddr, r)
|
109 | 158 |
}()
|
110 | |
logger.Log("err", <-errc)
|
111 | |
}
|
112 | |
|
113 | |
func makeHandler(ctx context.Context, e endpoint.Endpoint, logger log.Logger) http.HandlerFunc {
|
114 | |
return func(w http.ResponseWriter, r *http.Request) {
|
115 | |
resp, err := e(ctx, r.Body)
|
|
159 |
|
|
160 |
// Run!
|
|
161 |
logger.Log("exit", <-errc)
|
|
162 |
}
|
|
163 |
|
|
164 |
func addsvcFactory(makeEndpoint func(addsvc.Service) endpoint.Endpoint, tracer stdopentracing.Tracer, logger log.Logger) sd.Factory {
|
|
165 |
return func(instance string) (endpoint.Endpoint, io.Closer, error) {
|
|
166 |
// We could just as easily use the HTTP or Thrift client package to make
|
|
167 |
// the connection to addsvc. We've chosen gRPC arbitrarily. Note that
|
|
168 |
// the transport is an implementation detail: it doesn't leak out of
|
|
169 |
// this function. Nice!
|
|
170 |
|
|
171 |
conn, err := grpc.Dial(instance, grpc.WithInsecure())
|
116 | 172 |
if err != nil {
|
117 | |
logger.Log("err", err)
|
118 | |
http.Error(w, err.Error(), http.StatusInternalServerError)
|
119 | |
return
|
120 | |
}
|
121 | |
b, ok := resp.([]byte)
|
122 | |
if !ok {
|
123 | |
logger.Log("err", "endpoint response is not of type []byte")
|
124 | |
http.Error(w, err.Error(), http.StatusInternalServerError)
|
125 | |
return
|
126 | |
}
|
127 | |
_, err = w.Write(b)
|
128 | |
if err != nil {
|
129 | |
logger.Log("err", err)
|
130 | |
return
|
131 | |
}
|
132 | |
}
|
133 | |
}
|
134 | |
|
135 | |
func makeSumEndpoint(svc server.AddService) endpoint.Endpoint {
|
136 | |
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
137 | |
r := request.(io.Reader)
|
138 | |
var req server.SumRequest
|
139 | |
if err := json.NewDecoder(r).Decode(&req); err != nil {
|
140 | |
return nil, err
|
141 | |
}
|
142 | |
v := svc.Sum(req.A, req.B)
|
143 | |
return json.Marshal(v)
|
144 | |
}
|
145 | |
}
|
146 | |
|
147 | |
func makeConcatEndpoint(svc server.AddService) endpoint.Endpoint {
|
148 | |
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
149 | |
r := request.(io.Reader)
|
150 | |
var req server.ConcatRequest
|
151 | |
if err := json.NewDecoder(r).Decode(&req); err != nil {
|
152 | |
return nil, err
|
153 | |
}
|
154 | |
v := svc.Concat(req.A, req.B)
|
155 | |
return json.Marshal(v)
|
156 | |
}
|
157 | |
}
|
158 | |
|
159 | |
func httpFactory(ctx context.Context, method, path string) loadbalancer.Factory {
|
|
173 |
return nil, nil, err
|
|
174 |
}
|
|
175 |
service := addsvcgrpcclient.New(conn, tracer, logger)
|
|
176 |
endpoint := makeEndpoint(service)
|
|
177 |
|
|
178 |
// Notice that the addsvc gRPC client converts the connection to a
|
|
179 |
// complete addsvc, and we just throw away everything except the method
|
|
180 |
// we're interested in. A smarter factory would mux multiple methods
|
|
181 |
// over the same connection. But that would require more work to manage
|
|
182 |
// the returned io.Closer, e.g. reference counting. Since this is for
|
|
183 |
// the purposes of demonstration, we'll just keep it simple.
|
|
184 |
|
|
185 |
return endpoint, conn, nil
|
|
186 |
}
|
|
187 |
}
|
|
188 |
|
|
189 |
func stringsvcFactory(ctx context.Context, method, path string) sd.Factory {
|
160 | 190 |
return func(instance string) (endpoint.Endpoint, io.Closer, error) {
|
161 | |
var e endpoint.Endpoint
|
162 | 191 |
if !strings.HasPrefix(instance, "http") {
|
163 | 192 |
instance = "http://" + instance
|
164 | 193 |
}
|
165 | |
u, err := url.Parse(instance)
|
|
194 |
tgt, err := url.Parse(instance)
|
166 | 195 |
if err != nil {
|
167 | 196 |
return nil, nil, err
|
168 | 197 |
}
|
169 | |
u.Path = path
|
170 | |
|
171 | |
e = httptransport.NewClient(method, u, passEncode, passDecode).Endpoint()
|
172 | |
return e, nil, nil
|
173 | |
}
|
174 | |
}
|
175 | |
|
176 | |
func passEncode(_ context.Context, r *http.Request, request interface{}) error {
|
177 | |
r.Body = request.(io.ReadCloser)
|
|
198 |
tgt.Path = path
|
|
199 |
|
|
200 |
// Since stringsvc doesn't have any kind of package we can import, or
|
|
201 |
// any formal spec, we are forced to just assert where the endpoints
|
|
202 |
// live, and write our own code to encode and decode requests and
|
|
203 |
// responses. Ideally, if you write the service, you will want to
|
|
204 |
// provide stronger guarantees to your clients.
|
|
205 |
|
|
206 |
var (
|
|
207 |
enc httptransport.EncodeRequestFunc
|
|
208 |
dec httptransport.DecodeResponseFunc
|
|
209 |
)
|
|
210 |
switch path {
|
|
211 |
case "/uppercase":
|
|
212 |
enc, dec = encodeJSONRequest, decodeUppercaseResponse
|
|
213 |
case "/count":
|
|
214 |
enc, dec = encodeJSONRequest, decodeCountResponse
|
|
215 |
default:
|
|
216 |
return nil, nil, fmt.Errorf("unknown stringsvc path %q", path)
|
|
217 |
}
|
|
218 |
|
|
219 |
return httptransport.NewClient(method, tgt, enc, dec).Endpoint(), nil, nil
|
|
220 |
}
|
|
221 |
}
|
|
222 |
|
|
223 |
func encodeJSONRequest(_ context.Context, req *http.Request, request interface{}) error {
|
|
224 |
// Both uppercase and count requests are encoded in the same way:
|
|
225 |
// simple JSON serialization to the request body.
|
|
226 |
var buf bytes.Buffer
|
|
227 |
if err := json.NewEncoder(&buf).Encode(request); err != nil {
|
|
228 |
return err
|
|
229 |
}
|
|
230 |
req.Body = ioutil.NopCloser(&buf)
|
178 | 231 |
return nil
|
179 | 232 |
}
|
180 | 233 |
|
181 | |
func passDecode(_ context.Context, r *http.Response) (interface{}, error) {
|
182 | |
return ioutil.ReadAll(r.Body)
|
183 | |
}
|
184 | |
|
185 | |
func interrupt() error {
|
186 | |
c := make(chan os.Signal)
|
187 | |
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
188 | |
return fmt.Errorf("%s", <-c)
|
189 | |
}
|
|
234 |
func encodeJSONResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
|
235 |
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
236 |
return json.NewEncoder(w).Encode(response)
|
|
237 |
}
|
|
238 |
|
|
239 |
// I've just copied these functions from stringsvc3/transport.go, inlining the
|
|
240 |
// struct definitions.
|
|
241 |
|
|
242 |
func decodeUppercaseResponse(ctx context.Context, resp *http.Response) (interface{}, error) {
|
|
243 |
var response struct {
|
|
244 |
V string `json:"v"`
|
|
245 |
Err string `json:"err,omitempty"`
|
|
246 |
}
|
|
247 |
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
|
248 |
return nil, err
|
|
249 |
}
|
|
250 |
return response, nil
|
|
251 |
}
|
|
252 |
|
|
253 |
func decodeCountResponse(ctx context.Context, resp *http.Response) (interface{}, error) {
|
|
254 |
var response struct {
|
|
255 |
V int `json:"v"`
|
|
256 |
}
|
|
257 |
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
|
258 |
return nil, err
|
|
259 |
}
|
|
260 |
return response, nil
|
|
261 |
}
|
|
262 |
|
|
263 |
func decodeUppercaseRequest(ctx context.Context, req *http.Request) (interface{}, error) {
|
|
264 |
var request struct {
|
|
265 |
S string `json:"s"`
|
|
266 |
}
|
|
267 |
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
|
268 |
return nil, err
|
|
269 |
}
|
|
270 |
return request, nil
|
|
271 |
}
|
|
272 |
|
|
273 |
func decodeCountRequest(ctx context.Context, req *http.Request) (interface{}, error) {
|
|
274 |
var request struct {
|
|
275 |
S string `json:"s"`
|
|
276 |
}
|
|
277 |
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
|
278 |
return nil, err
|
|
279 |
}
|
|
280 |
return request, nil
|
|
281 |
}
|