From e77fcae81ca2505b15784b9160932d018c80ced3 Mon Sep 17 00:00:00 2001 From: Philip Sahli Date: Fri, 4 Jan 2019 22:43:26 +0100 Subject: [PATCH] add gRPC service --- .gitignore | 2 +- Dockerfile | 2 +- Gopkg.lock | 115 +++++++++++++++++++++ Gopkg.toml | 4 + README.md | 33 +++++- deploy/build.yaml | 2 +- deploy/deploy.sh | 2 +- deploy/route.yaml | 6 +- deploy/service.yaml | 72 +++++++++---- deploy/template.yaml | 28 +++++- service/gontador.pb.go | 222 +++++++++++++++++++++++++++++++++++++++++ service/gontador.proto | 17 ++++ src/counter.go | 20 +++- src/main.go | 39 ++++++++ 14 files changed, 525 insertions(+), 39 deletions(-) create mode 100644 service/gontador.pb.go create mode 100644 service/gontador.proto diff --git a/.gitignore b/.gitignore index 7a6beb4..602f834 100644 --- a/.gitignore +++ b/.gitignore @@ -63,4 +63,4 @@ typings/ # gontador .vscode vendor -main +gontador \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 3869529..54e1a27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ RUN chmod +x /usr/bin/dep # Copy the code from the host and compile it WORKDIR $GOPATH/src/github.com/philipsahli/gontador/ COPY Gopkg.toml Gopkg.lock ./ -RUN dep ensure --vendor-only +RUN dep ensure --vendor-only -v COPY . ./ RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix nocgo -o /app github.com/philipsahli/gontador/src diff --git a/Gopkg.lock b/Gopkg.lock index 2ebbc99..562e164 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -26,6 +26,21 @@ revision = "7f89fbac80bcc62ce920b6dbc6ca60238d7725d1" version = "v6.15.0" +[[projects]] + branch = "master" + digest = "1:a54f931f516df9f3b2401e3cfa47482be79397d20fcbe838b7da6c63d5b8e615" + name = "github.com/golang/protobuf" + packages = [ + "proto", + "protoc-gen-go/descriptor", + "ptypes", + "ptypes/any", + "ptypes/duration", + "ptypes/timestamp", + ] + pruneopts = "UT" + revision = "1d3f30b51784bec5aad268e59fd3c2fc1c2fe73f" + [[projects]] digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1" name = "github.com/gorilla/context" @@ -42,13 +57,113 @@ revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf" version = "v1.6.2" +[[projects]] + branch = "master" + digest = "1:89a0cb976397aa9157a45bb2b896d0bcd07ee095ac975e0f03c53250c402265e" + name = "golang.org/x/net" + packages = [ + "context", + "http/httpguts", + "http2", + "http2/hpack", + "idna", + "internal/timeseries", + "trace", + ] + pruneopts = "UT" + revision = "927f97764cc334a6575f4b7a1584a147864d5723" + +[[projects]] + branch = "master" + digest = "1:5004e851e5eccde563d17871cd9d11c82e2faa578b1a0de81dc74867ad3845a4" + name = "golang.org/x/sys" + packages = ["unix"] + pruneopts = "UT" + revision = "badf5585203e739f88c2c6cd34188a6f54b5d619" + +[[projects]] + digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18" + name = "golang.org/x/text" + packages = [ + "collate", + "collate/build", + "internal/colltab", + "internal/gen", + "internal/tag", + "internal/triegen", + "internal/ucd", + "language", + "secure/bidirule", + "transform", + "unicode/bidi", + "unicode/cldr", + "unicode/norm", + "unicode/rangetable", + ] + pruneopts = "UT" + revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" + version = "v0.3.0" + +[[projects]] + branch = "master" + digest = "1:077c1c599507b3b3e9156d17d36e1e61928ee9b53a5b420f10f28ebd4a0b275c" + name = "google.golang.org/genproto" + packages = ["googleapis/rpc/status"] + pruneopts = "UT" + revision = "bd9b4fb69e2ffd37621a6caa54dcbead29b546f2" + +[[projects]] + digest = "1:8c8ed249fa6a8db070bf2082f02052c697695fa5e1558b4e28dd0fb5f15f70a2" + name = "google.golang.org/grpc" + packages = [ + ".", + "balancer", + "balancer/base", + "balancer/roundrobin", + "binarylog/grpc_binarylog_v1", + "codes", + "connectivity", + "credentials", + "credentials/internal", + "encoding", + "encoding/proto", + "grpclog", + "internal", + "internal/backoff", + "internal/binarylog", + "internal/channelz", + "internal/envconfig", + "internal/grpcrand", + "internal/grpcsync", + "internal/syscall", + "internal/transport", + "keepalive", + "metadata", + "naming", + "peer", + "reflection", + "reflection/grpc_reflection_v1alpha", + "resolver", + "resolver/dns", + "resolver/passthrough", + "stats", + "status", + "tap", + ] + pruneopts = "UT" + revision = "df014850f6dee74ba2fc94874043a9f3f75fbfd8" + version = "v1.17.0" + [solve-meta] analyzer-name = "dep" analyzer-version = 1 input-imports = [ "github.com/almariah/go-graphite-client", "github.com/go-redis/redis", + "github.com/golang/protobuf/proto", "github.com/gorilla/mux", + "google.golang.org/grpc", + "google.golang.org/grpc/reflection", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 5c6b302..d3e8179 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -10,6 +10,7 @@ # name = "github.com/user/project" # version = "1.0.0" # + # [[constraint]] # name = "github.com/user/project2" # branch = "dev" @@ -24,6 +25,9 @@ # go-tests = true # unused-packages = true +[[constraint]] + branch = "master" + name = "github.com/golang/protobuf" [[constraint]] branch = "master" diff --git a/README.md b/README.md index ac1086a..f392cbf 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,20 @@ # contador + A 12-factor counter microservice -## Services +Exposes an HTTP-API and a gRPC Interface. + +## Dependant services -### Telegraf +**Telegraf** -Run somewhere +Run somewhere a simple tcp listener to mock a telegraf daemon. nc -l 2003 -### Redis +**Redis** -Run somewhere +Run somewhere a docker container with an unsecured redis daemon. docker run --name gontador-redis -p 6379:6379 -d redis @@ -23,8 +26,20 @@ Run somewhere ### HTTP Interface +A HTTP/1.1 GET request to `/counter` increments the counter by 1 and returns the new counter. + curl http://localhost:3000/counter +### gRPC Service + +Gonsumidor increments the counter by 1 and prints out the counter every 2 seconds. Communication over gRPC to the service on port `3001`. + + git clone https://github.com/philipsahli/gonsumidor.git + cd gonsumidor && go build -o gonsumidor main.go + go build -o gonsumidor main.go + +See https://github.com/philipsahli/gonsumidor/blob/master/README.md + ## Test on Openshift bash -xe deploy/deploy.sh @@ -37,3 +52,11 @@ Run somewhere oc delete all -l app=gontador oc delete template gontador-template + +## Generate grpc service + + PROTOC_ZIP=protoc-3.3.0-osx-x86_64.zip\ncurl -OL https://github.com/google/protobuf/releases/download/v3.3.0/$PROTOC_ZIP\nsudo unzip -o $PROTOC_ZIP -d /usr/local bin/protoc\nrm -f $PROTOC_ZIP + go get -u github.com/golang/protobuf/protoc-gen-go + + protoc -I service/ service/gontador.proto --go_out=plugins=grpc:service + diff --git a/deploy/build.yaml b/deploy/build.yaml index 7c8361d..e925e6f 100644 --- a/deploy/build.yaml +++ b/deploy/build.yaml @@ -23,7 +23,7 @@ spec: source: git: uri: 'https://github.com/philipsahli/gontador.git' - ref: 'go-implementation' + ref: 'grpc-service' type: Git strategy: type: Docker diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 78e46d3..c0300bb 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -1,4 +1,4 @@ -#!/bin/bash -xe +#!/bin/bash -x # Build docker image in openshift # oc apply -f imagestream.yaml diff --git a/deploy/route.yaml b/deploy/route.yaml index c566e92..e64cb5a 100644 --- a/deploy/route.yaml +++ b/deploy/route.yaml @@ -6,7 +6,7 @@ metadata: creationTimestamp: '2018-12-31T10:24:53Z' labels: app: gontador - name: gontador + name: gontador-http namespace: gontador resourceVersion: '58060' selfLink: /apis/route.openshift.io/v1/namespaces/gontador/routes/gontador @@ -14,10 +14,10 @@ metadata: spec: host: gontador-gontador.192.168.64.7.nip.io port: - targetPort: gontador + targetPort: gontador-http to: kind: Service - name: gontador + name: gontador-http weight: 100 wildcardPolicy: None status: diff --git a/deploy/service.yaml b/deploy/service.yaml index e5f42a8..31d829d 100644 --- a/deploy/service.yaml +++ b/deploy/service.yaml @@ -1,23 +1,51 @@ apiVersion: v1 -kind: Service -metadata: - annotations: - description: Exposes the gontador service - creationTimestamp: '2018-12-31T06:50:19Z' - labels: - app: gontador - name: gontador - namespace: gontador - resourceVersion: '3000' -spec: - ports: - - name: gontador - port: 3000 - protocol: TCP - targetPort: 3000 - selector: - app: gontador - sessionAffinity: None - type: ClusterIP -status: - loadBalancer: {} +kind: List +items: +- apiVersion: v1 + kind: Service + metadata: + annotations: + description: Exposes the gontador service + creationTimestamp: '2018-12-31T06:50:19Z' + labels: + app: gontador + name: gontador-http + namespace: gontador + resourceVersion: '3000' + spec: + ports: + - name: gontador-http + port: 3000 + protocol: TCP + targetPort: 3000 + # - name: gontador-grpc + # port: 3001 + # protocol: TCP + # targetPort: 3001 + selector: + app: gontador + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} +- apiVersion: v1 + kind: Service + metadata: + annotations: + description: Exposes the gontador grpc service + creationTimestamp: '2018-12-31T06:50:19Z' + labels: + app: gontador + name: gontador-grpc + namespace: gontador + resourceVersion: '3000' + spec: + ports: + - name: gontador-grpc + port: 3001 + nodePort: 30001 + selector: + app: gontador + type: NodePort + status: + loadBalancer: {} \ No newline at end of file diff --git a/deploy/template.yaml b/deploy/template.yaml index 73abf13..8ea1125 100644 --- a/deploy/template.yaml +++ b/deploy/template.yaml @@ -4,22 +4,44 @@ metadata: creationTimestamp: null name: gontador-template objects: +- apiVersion: v1 + kind: Service + metadata: + annotations: + description: Exposes the gontador grpc service + labels: + app: gontador + name: gontador-grpc + namespace: gontador + spec: + ports: + - name: gontador-grpc + port: 3001 + nodePort: 30001 + selector: + app: gontador + type: NodePort + status: + loadBalancer: {} - apiVersion: v1 kind: Service metadata: annotations: description: Exposes the gontador service - creationTimestamp: '2018-12-31T06:50:19Z' labels: app: gontador - name: gontador + name: gontador-http namespace: gontador spec: ports: - - name: gontador + - name: gontador-http port: 3000 protocol: TCP targetPort: 3000 + # - name: gontador-grpc + # port: 3001 + # protocol: TCP + # targetPort: 3001 selector: app: gontador sessionAffinity: None diff --git a/service/gontador.pb.go b/service/gontador.pb.go new file mode 100644 index 0000000..a78418b --- /dev/null +++ b/service/gontador.pb.go @@ -0,0 +1,222 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: gontador.proto + +package gontador + +import ( + context "context" + fmt "fmt" + proto "github.com/golang/protobuf/proto" + grpc "google.golang.org/grpc" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +// The response message containing the Counter +type CountReply struct { + Counter int64 `protobuf:"varint,1,opt,name=Counter,proto3" json:"Counter,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CountReply) Reset() { *m = CountReply{} } +func (m *CountReply) String() string { return proto.CompactTextString(m) } +func (*CountReply) ProtoMessage() {} +func (*CountReply) Descriptor() ([]byte, []int) { + return fileDescriptor_56cfa82bfd58bba4, []int{0} +} + +func (m *CountReply) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CountReply.Unmarshal(m, b) +} +func (m *CountReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CountReply.Marshal(b, m, deterministic) +} +func (m *CountReply) XXX_Merge(src proto.Message) { + xxx_messageInfo_CountReply.Merge(m, src) +} +func (m *CountReply) XXX_Size() int { + return xxx_messageInfo_CountReply.Size(m) +} +func (m *CountReply) XXX_DiscardUnknown() { + xxx_messageInfo_CountReply.DiscardUnknown(m) +} + +var xxx_messageInfo_CountReply proto.InternalMessageInfo + +func (m *CountReply) GetCounter() int64 { + if m != nil { + return m.Counter + } + return 0 +} + +type Empty struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Empty) Reset() { *m = Empty{} } +func (m *Empty) String() string { return proto.CompactTextString(m) } +func (*Empty) ProtoMessage() {} +func (*Empty) Descriptor() ([]byte, []int) { + return fileDescriptor_56cfa82bfd58bba4, []int{1} +} + +func (m *Empty) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Empty.Unmarshal(m, b) +} +func (m *Empty) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Empty.Marshal(b, m, deterministic) +} +func (m *Empty) XXX_Merge(src proto.Message) { + xxx_messageInfo_Empty.Merge(m, src) +} +func (m *Empty) XXX_Size() int { + return xxx_messageInfo_Empty.Size(m) +} +func (m *Empty) XXX_DiscardUnknown() { + xxx_messageInfo_Empty.DiscardUnknown(m) +} + +var xxx_messageInfo_Empty proto.InternalMessageInfo + +func init() { + proto.RegisterType((*CountReply)(nil), "CountReply") + proto.RegisterType((*Empty)(nil), "Empty") +} + +func init() { proto.RegisterFile("gontador.proto", fileDescriptor_56cfa82bfd58bba4) } + +var fileDescriptor_56cfa82bfd58bba4 = []byte{ + // 118 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4b, 0xcf, 0xcf, 0x2b, + 0x49, 0x4c, 0xc9, 0x2f, 0xd2, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x57, 0x52, 0xe3, 0xe2, 0x72, 0xce, + 0x2f, 0xcd, 0x2b, 0x09, 0x4a, 0x2d, 0xc8, 0xa9, 0x14, 0x92, 0xe0, 0x62, 0x07, 0xf3, 0x52, 0x8b, + 0x24, 0x18, 0x15, 0x18, 0x35, 0x98, 0x83, 0x60, 0x5c, 0x25, 0x76, 0x2e, 0x56, 0xd7, 0xdc, 0x82, + 0x92, 0x4a, 0x23, 0x3f, 0xb8, 0x12, 0x21, 0x39, 0x2e, 0x56, 0x30, 0x53, 0x88, 0x4d, 0x0f, 0x2c, + 0x27, 0xc5, 0xad, 0x87, 0x30, 0x4b, 0x89, 0x41, 0x48, 0x99, 0x8b, 0xcb, 0x3d, 0xb5, 0x04, 0xa6, + 0x1a, 0xbb, 0xa2, 0x24, 0x36, 0xb0, 0x3b, 0x8c, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0x10, 0x38, + 0x95, 0x7a, 0x99, 0x00, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// CounterClient is the client API for Counter service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type CounterClient interface { + // Sends a greeting + Count(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*CountReply, error) + // Sends another greeting + GetCounter(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*CountReply, error) +} + +type counterClient struct { + cc *grpc.ClientConn +} + +func NewCounterClient(cc *grpc.ClientConn) CounterClient { + return &counterClient{cc} +} + +func (c *counterClient) Count(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*CountReply, error) { + out := new(CountReply) + err := c.cc.Invoke(ctx, "/Counter/Count", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *counterClient) GetCounter(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*CountReply, error) { + out := new(CountReply) + err := c.cc.Invoke(ctx, "/Counter/GetCounter", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// CounterServer is the server API for Counter service. +type CounterServer interface { + // Sends a greeting + Count(context.Context, *Empty) (*CountReply, error) + // Sends another greeting + GetCounter(context.Context, *Empty) (*CountReply, error) +} + +func RegisterCounterServer(s *grpc.Server, srv CounterServer) { + s.RegisterService(&_Counter_serviceDesc, srv) +} + +func _Counter_Count_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CounterServer).Count(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/Counter/Count", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CounterServer).Count(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _Counter_GetCounter_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CounterServer).GetCounter(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/Counter/GetCounter", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CounterServer).GetCounter(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +var _Counter_serviceDesc = grpc.ServiceDesc{ + ServiceName: "Counter", + HandlerType: (*CounterServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Count", + Handler: _Counter_Count_Handler, + }, + { + MethodName: "GetCounter", + Handler: _Counter_GetCounter_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "gontador.proto", +} diff --git a/service/gontador.proto b/service/gontador.proto new file mode 100644 index 0000000..ae1396f --- /dev/null +++ b/service/gontador.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +// The greeting service definition. +service Counter { + // Sends a greeting + rpc Count (Empty) returns (CountReply) {} + // Sends another greeting + rpc GetCounter (Empty) returns (CountReply) {} +} + +// The response message containing the Counter +message CountReply { + int64 Counter = 1; +} + +message Empty { +} \ No newline at end of file diff --git a/src/counter.go b/src/counter.go index eb8d5f7..d23a930 100644 --- a/src/counter.go +++ b/src/counter.go @@ -7,12 +7,28 @@ import ( "strconv" ) -func count(w http.ResponseWriter, r *http.Request) { - result, err := redisdb.IncrBy("counter1", 1).Result() +func get() int64 { + result, err := redisdb.Get("counter1").Result() //time.Sleep(5 * time.Second) if err != nil { panic(err) } + r64, _ := strconv.ParseInt(result, 10, 64) + return r64 + // return result + +} + +func incr() int64 { + result, err := redisdb.IncrBy("counter1", 1).Result() + if err != nil { + panic(err) + } + return result +} + +func count(w http.ResponseWriter, r *http.Request) { + result := incr() cs := strconv.FormatInt(result, 10) m := fmt.Sprintf("%s.gontador.%s.%s.counter.value", diff --git a/src/main.go b/src/main.go index 8b891d9..b53bd10 100644 --- a/src/main.go +++ b/src/main.go @@ -4,6 +4,7 @@ import ( "context" "flag" "log" + "net" "net/http" "os" "os/signal" @@ -13,6 +14,10 @@ import ( graphite "github.com/almariah/go-graphite-client" "github.com/go-redis/redis" "github.com/gorilla/mux" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" + + pb "github.com/philipsahli/gontador/service" ) var redisdb *redis.Client @@ -20,6 +25,24 @@ var graphiteClient *graphite.Client var traceID string var isReady bool +const ( + port = ":3001" +) + +type server struct{} + +func (s *server) Count(ctx context.Context, in *pb.Empty) (*pb.CountReply, error) { + log.Printf("Received grpc Count request") + counter := incr() + return &pb.CountReply{Counter: counter}, nil +} + +func (s *server) GetCounter(ctx context.Context, in *pb.Empty) (*pb.CountReply, error) { + log.Printf("Received grpc GetCounter request") + counter := get() + return &pb.CountReply{Counter: counter}, nil +} + func init() { // Configure Logger @@ -59,6 +82,8 @@ func main() { flag.DurationVar(&wait, "graceful-timeout", time.Second*15, "the duration for which the server gracefully wait for existing connections to finish - e.g. 15s or 1m") flag.Parse() + log.Println("grpc started on port 3001") + r := mux.NewRouter() r.HandleFunc("/counter", count).Methods("GET") r.HandleFunc("/health/ready", ready).Methods("GET") @@ -75,6 +100,20 @@ func main() { } }() + lis, err := net.Listen("tcp", port) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + s := server{} + grpcServer := grpc.NewServer() + // pb.CounterServer(grpcServer, &s) + pb.RegisterCounterServer(grpcServer, &s) + // Register reflection service on gRPC server. + reflection.Register(grpcServer) + if err := grpcServer.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } + c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt)