Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Line Bot on Cloud Run: 使用 line/line-bot-sdk-go

Line Bot on Cloud Run: 使用 line/line-bot-sdk-go

- My five years of go
- How-To: line/line-bot-sdk-go
-- Simple echo & face recognition chatbot
- Develop/test with ngrok
- Develop with docker-compose
- Deploy to Google Cloud Runbeta
- DevOps: Logging/Tracing/Metrics

Cheng-Lung Sung

June 28, 2019
Tweet

More Decks by Cheng-Lung Sung

Other Decks in Programming

Transcript

  1. Outline • My five years of go • How-To: line/line-bot-sdk-go

    • Simple echo & face recognition chatbot • Develop/test with ngrok • Develop with docker-compose • Deploy to Google Cloud Runbeta • DevOps 三寶: Logging/Tracing/Metrics
  2. // FaceDetect detects face and return json data func FaceDetect(reader

    io.Reader) (ret []byte, err error) { sess := session.New(&aws.Config{ Region: aws.String("us-west-2"), }) svc := rekognition.New(sess) // Read buf to byte[] b, err := ioutil.ReadAll(reader) if err != nil { return b, err } // Send the request to Rekognition input := &rekognition.DetectFacesInput{ Image: &rekognition.Image{ Bytes: b, }, Attributes: []*string{aws.String("ALL")}, } result, err := svc.DetectFaces(input) if err != nil { log.Print(err) return ret, err } return []byte(fmt.Sprintf("AWS Faces: %d", len(result.FaceDetails))), nil }
  3. // FaceDetect detects face and return json data func FaceDetect(reader

    io.Reader) (ret []byte, err error) { ctx := context.Background() // client, err := vision.NewImageAnnotatorClient(ctx, option.WithCredentialsFile(jsonPath)) client, err := vision.NewImageAnnotatorClient(ctx) if err != nil { log.Printf("Failed to create client: %v", err) return ret, err } defer client.Close() image, err := vision.NewImageFromReader(reader) if err != nil { log.Printf("Failed to create image: %v", err) return ret, err } faces, err := client.DetectFaces(ctx, image, nil, 10) if err != nil { log.Printf("Failed to detect labels: %v", err) return ret, err } return []byte(fmt.Sprintf("GCP Faces: %d", len(faces))), nil }
  4. func main() { app := cvbot.NewCVApp() // Setup HTTP Server

    for receiving requests from LINE platform http.HandleFunc("/webhook", func(w http.ResponseWriter, req *http.Request) { events, err := app.ParseRequest(req.Context(), req) if err != nil { if err == linebot.ErrInvalidSignature { w.WriteHeader(400) } else { w.WriteHeader(500) } return } v, err := app.Webhook(req.Context(), events) log.Printf("events: %d, err: %v", v, err) }) http.HandleFunc("/health", func(w http.ResponseWriter, req *http.Request) { _, err := w.Write([]byte(`{"health": "OK"}`)) if err != nil { log.Fatal(err) } }) port := os.Getenv("PORT") if port == "" { port = "8080" } log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil)) }
  5. // CVApp app type CVApp struct { bot *linebot.Client }

    // NewCVApp returns a service func NewCVApp() Service { bot, err := linebot.New( os.Getenv("LINE_CHANNEL_SECRET"), os.Getenv("LINE_CHANNEL_TOKEN"), ) if err != nil { log.Fatal(err) } return &CVApp{ bot: bot, } } // ParseRequest parse http.Request into events func (app *CVApp) ParseRequest(ctx context.Context, r *http.Request) return app.bot.ParseRequest(r) } // Webhook handles parsed events func (app *CVApp) Webhook(ctx context.Context, r interface{}) (int, e events := r.([]*linebot.Event) for _, event := range events { replyMsgs, err := app.HandleEvent(ctx, event) if err != nil { return 0, errors.Wrap(err, "handle event failed") } if _, err := app.bot.ReplyMessage( event.ReplyToken, replyMsgs..., ).Do(); err != nil { return 0, errors.Wrap(err, "reply message failed") } } return len(events), nil }
  6. // HandleEvent implements event handler with provider-based logic func (app

    *CVApp) HandleEvent(ctx context.Context, event *linebot.Event) (replyMsgs []linebot.SendingMessage, err error) { if event.Type == linebot.EventTypeMessage { switch message := event.Message.(type) { case *linebot.TextMessage: replyMsgs = append(replyMsgs, linebot.NewTextMessage(message.Text)) case *linebot.ImageMessage: var retMsgs []linebot.SendingMessage if retMsgs, err = app.handleImage(ctx, message); err != nil { return replyMsgs, errors.Wrap(err, "handle image failed") } replyMsgs = append(replyMsgs, retMsgs...) case *linebot.StickerMessage: return replyMsgs, errors.New("STICKER ERROR") default: replyMsgs = append(replyMsgs, linebot.NewTextMessage("not supported yet")) } } return replyMsgs, nil }
  7. func (app *CVApp) handleImage(ctx context.Context, message *linebot.ImageMessage) (replyMsgs []linebot.SendingMessage, err

    error) { _, err = app.handleFaceRecognition(ctx, message.ID, func(gcpBuf io.Reader, awsBuf io.Reader) error { awsFaces, err := aws.FaceDetect(awsBuf) if err != nil { return errors.Wrap(err, "aws face detect error") } replyMsgs = append(replyMsgs, linebot.NewTextMessage(string(awsFaces))) gcpFaces, err := gcp.FaceDetect(gcpBuf) if err != nil { return errors.Wrap(err, "gcp face detect error") } replyMsgs = append(replyMsgs, linebot.NewTextMessage(string(gcpFaces))) return nil }) return replyMsgs, err } func (app *CVApp) handleFaceRecognition(ctx context.Context, messageID string, callback func(io.Reader, io.Reader) error) ([]linebot.SendingMessage, error) { content, err := app.bot.GetMessageContent(messageID).Do() if err != nil { return nil, errors.Wrap(err, "get image from line error") } defer content.Content.Close() var gcpBuf, awsBuf bytes.Buffer w := io.MultiWriter(&gcpBuf, &awsBuf) if _, err := io.Copy(w, content.Content); err != nil { return nil, errors.Wrap(err, "multiwriter copy error") } return nil, callback(io.Reader(&gcpBuf), io.Reader(&awsBuf))
  8. version: '3' services: chatbot: build: context: ../ dockerfile: deploy/Dockerfile command:

    simple #env_file: # - .env ports: - "8080:8080" environment: - PORT=8080 - LINE_CHANNEL_SECRET=${LINE_CHANNEL_SECRET} - LINE_CHANNEL_TOKEN=${LINE_CHANNEL_TOKEN} - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - GOOGLE_APPLICATION_CREDENTIALS=/google-cred.json volumes: - "${GOOGLE_APPLICATION_CREDENTIALS}:/google-cred.json:ro" docker-compose.yml
  9. FROM golang:1.12-alpine as builder RUN apk update && apk upgrade

    && \ apk add --no-cache bash \ bind-tools \ git # Install openfortivpn RUN apk --update upgrade \ && apk add ca-certificates wget \ && update-ca-certificates ENV GO111MODULE off RUN go get -u "github.com/aws/aws-sdk-go/aws" \ "github.com/aws/aws-sdk-go/aws/session" \ "github.com/aws/aws-sdk-go/service/rekognition" \ "cloud.google.com/go/vision/apiv1" \ "github.com/sirupsen/logrus" \ "go.opencensus.io/trace" \ "contrib.go.opencensus.io/exporter/stackdriver" \ "github.com/clsung/logger" \ "github.com/pkg/errors" \ "github.com/line/line-bot-sdk-go/linebot" COPY . /go/src/github.com/clsung/cvbot/ WORKDIR /go/src/github.com/clsung/cvbot/cmd/tracesim/ RUN go build -o /go/bin/cvbot FROM alpine:3.9 RUN apk --no-cache add ca-certificates openssl ppp curl su-exec bash && rm -rf /var/cache/apk/*; COPY --from=builder /go/bin/cvbot /usr/bin/cvbot CMD ["/usr/bin/cvbot"]
  10. Container runtime contract • The container is compiled for 64-bit

    Linux. • The container listens for HTTP requests on the port defined by the PORT environment variable, which is always set to 8080. • Can fit in up to 2 GB of memory (initially 256MB). • Container instances must start an HTTP server within 4 minutes after receiving a request. • Default timeout: 5mins • Revision should work as containers are auto-scaled from 0 to multiple running instances. • When a revision does not receive any traffic, it is scaled down to zero instances • All computation is stateless and scoped to a request.
  11. #!/usr/bin/env bash # gcloud config set project {PROJECTID} DIR="$( cd

    "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" HOSTNAME=asia.gcr.io PROJECTID=${LINE_PROJECT} IMAGE=cvbot TAG=`git rev-parse --short=7 HEAD` cd "${DIR}/.." docker build -t ${HOSTNAME}/${PROJECTID}/${IMAGE}:${TAG} -f deploy/Dockerfile . if [[ ${?} != 0 ]]; then # build error exit $? fi docker push ${HOSTNAME}/${PROJECTID}/${IMAGE}:${TAG} gcloud beta run deploy ${IMAGE} --project ${PROJECTID} --image ${HOSTNAME}/${PROJECTID}/${IMAGE}:${TAG} --region us-central1 -- platform managed
  12. –Gene Kim, The Phoenix Project: A Novel About IT, DevOps,

    and Helping Your Business Win “Any improvements made anywhere besides the bottleneck are an illusion”
  13. func main() { app := cvbot.LoggingMiddleware(logger.New())(cvbot.NewCVApp()) // Setup HTTP Server

    for receiving requests from LINE platform http.HandleFunc("/webhook", func(w http.ResponseWriter, req *http.Request) { events, err := app.ParseRequest(req.Context(), req) log.Printf("events: %d, err: %v", events, err) if err != nil { if err == linebot.ErrInvalidSignature { w.WriteHeader(400) } else { w.WriteHeader(500) } return } app.Webhook(req.Context(), events) }) http.HandleFunc("/health", func(w http.ResponseWriter, req *http.Request) { _, err := w.Write([]byte(`{"health": "OK"}`)) if err != nil { log.Fatal(err) } }) port := os.Getenv("PORT") if port == "" { port = "8080" } log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil)) }
  14. // Service describes a service that adds things together. type

    Service interface { Webhook(context.Context, interface{}) (int, error) ParseRequest(context.Context, *http.Request) (interface{}, error) } // Middleware describes a service (as opposed to endpoint) middleware. type Middleware func(Service) Service // LoggingMiddleware returns a service middleware that logs the // parameters and result of each method invocation. func LoggingMiddleware(logger Logger) Middleware { return func(next Service) Service { return loggingMiddleware{ logger: logger, next: next, } } }
  15. func (mw loggingMiddleware) ParseRequest(ctx context.Context, r *http.Request) (v interface{}, err

    error) { defer func(begin time.Time) { mw.logger.Printf("events: %v, time: %v, error: %v", v, time.Since(begin), err) }(time.Now()) return mw.next.ParseRequest(ctx, r) } func (mw loggingMiddleware) Webhook(ctx context.Context, r interface{}) (v int, err error) { defer func(begin time.Time) { if log, ok := mw.logger.(*teltech.Log); ok { if err == nil { log.With(log.Fields{"result": v, "error": err, "elapsed": time.Since(begin)}).Info("Processed") } else { log.With(log.Fields{“result": v, "error": err, "elapsed": time.Since(begin)}).Error("Failed") } } else { mw.logger.Printf("result: %v, time: %v, error: %v", v, time.Since(begin), err) } }(time.Now()) return mw.next.Webhook(ctx, r) }
  16. func main() { trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()}) sd, err := stackdriver.NewExporter(stackdriver.Options{ ProjectID:

    "alansandbox", // MetricPrefix helps uniquely identify your metrics. MetricPrefix: "tracesim", }) if err != nil { log.Fatalf("Failed to create the Stackdriver exporter: %v", err) } // It is imperative to invoke flush before your main function exits defer sd.Flush() // Register it as a trace exporter trace.RegisterExporter(sd) app := cvbot.LoggingMiddleware(logger.New())(cvbot.NewCVApp()) app = cvbot.TraceMiddleware()(app) // setup trace config, production please use trace.ProbabilitySampler // Setup HTTP Server for receiving requests from LINE platform http.HandleFunc("/webhook", func(w http.ResponseWriter, req *http.Request) { ctx, span := trace.StartSpan(req.Context(), "webhook") defer span.End() events, err := app.ParseRequest(ctx, req)
  17. func (app *CVApp) handleImageAsync(ctx context.Context, message *linebot.ImageMessage) (replyMsgs []linebot.SendingMessage, err

    error) { ctx, span := trace.StartSpan(ctx, "handle_image_async") defer span.End() _, err = app.handleFaceRecognition(ctx, message.ID, func(gcpBuf io.Reader, awsBuf io.Reader) error { dataCh := make(chan string, 1) go func() { _, span := trace.StartSpan(ctx, "aws") awsFaces, _ := aws.FaceDetect(awsBuf) span.End() dataCh <- string(awsFaces) return }() go func() { _, span := trace.StartSpan(ctx, "gcp") gcpFaces, _ := gcp.FaceDetect(gcpBuf) span.End() dataCh <- string(gcpFaces) return }() ret := <-dataCh replyMsgs = append(replyMsgs, linebot.NewTextMessage(string(ret))) return nil }) return replyMsgs, err }