Slide 1

Slide 1 text

Logging in Microservices Paris API Meetup #30 7 November 2017 Leo Unbekandt CTO at Scalingo

Slide 2

Slide 2 text

What this lightning talk will cover Handling logs in and across Go services Not indexing, aggregating nor ELK-ing → Goal prepare the logs for these (Would love to, but 5 minutes is not enough)

Slide 3

Slide 3 text

Logging is hard Even in one single service. func main() { http.HandleFunc("/", Handler) http.ListenAndServe(":8001", nil) } func Handler(w http.ResponseWriter, r *http.Request) { log.Println("A user is doing something here") } 2017/11/04 11:21:07 A user is doing something here Yay… well, thanks stdlib, super useful.

Slide 4

Slide 4 text

Using a logging middleware func logMiddleware(next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("Before request", r.URL) before := time.Now() next.ServeHTTP(w, r) duration := time.Now().Sub(before) log.Println("Before request", r.URL, "duration", duration) }) } 2017/11/04 11:21:45 Before request /paris-api 2017/11/04 11:21:45 A user is doing something here 2017/11/04 11:21:45 Before request /paris-api duration 15.793µs Slightly better.

Slide 5

Slide 5 text

Next step is formatted logging func main() { applogger := logrus.New() applogger.Formatter = &logrus.TextFormatter{ TimestampFormat: "2006-01-02T15:04:05.000", FullTimestamp: true, } http.HandleFunc("/", logMiddleware(Handler, applogger)) http.ListenAndServe(":8003", nil) } func logMiddleware(next http.HandlerFunc, logger logrus.FieldLogger) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fields := logrus.Fields{ "method": r.Method, "path": r.URL.String(), "from": r.RemoteAddr, } reqlogger := logger.WithFields(fields) reqlogger.Info("starting request") // …

Slide 6

Slide 6 text

Next step is formatted logging // … rw := negroni.NewResponseWriter(w) before := time.Now() next(rw, r) after := time.Now() reqlogger.WithFields(logrus.Fields{ "status": rw.Status(), "duration": after.Sub(before).Seconds(), "bytes": rw.Size(), }).Info("request completed") }) } INFO[TS] starting request from="[::1]:46250" method=GET path="/paris-api" INFO[TS] A user is doing something here INFO[TS] request completed bytes=20 duration=2.3522e-05 from="[::1]:46250" host="localhost:8003" method=GET path="/paris-api" status=200

Slide 7

Slide 7 text

Gather all log lines from a request func RequestIDMiddleware(next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := r.Header.Get("X-Request-ID") if len(id) == 0 { id = uuid.NewV4().String() r.Header.Set("X-Request-ID", id) } r = r.WithContext(context.WithValue(r.Context(), "request_id", id)) next(w, r) }) } And in the logging middleware: id, ok := r.Context().Value("request_id").(string) if ok { logger = logger.WithField("request_id", id) }

Slide 8

Slide 8 text

Con gure your reverse proxy Most HTTP servers like Nginx, HAProxy, Apache have modules to add a X-Request-ID header. Also builtin in PaaS (Scalingo / Heroku) doc.scalingo.com/internals/x-request-id (http://doc.scalingo.com/internals/x-request-id) devcenter.heroku.com/articles/http-request-id (https://devcenter.heroku.com/articles/http-request-id)

Slide 9

Slide 9 text

Forward this Request-ID req, _ := http.NewRequest("GET", url, nil) reqID, ok := ctx.Value("request_id").(string) if ok && len(reqID) != 0 { req.Header.Set("X-Request-ID", reqID) }

Slide 10

Slide 10 text

Outside HTTP: Message queues Request ID in the metadata of the message: type Message struct { RequestID string `json:"request_id"` Type string `json:"type"` At int64 `json:"at"` Payload json.RawMessage `json:"payload"` }

Slide 11

Slide 11 text

Outside HTTP : Message queue producers func (p *Producer) Produce(ctx context.Context, topic string, message Message) error { reqid, ok := ctx.Value("request_id").(string) if !ok { reqid = uuid.NewV4().String() } message.RequestID = reqid // Really produce message }

Slide 12

Slide 12 text

Outside HTTP : Message queue consumers → Create middlewares to get/add the Request ID from/to the context. func (c *Consumer) Start(ctx context.Context) func() { c.logger = logctx.Get(ctx).WithFields(logrus.Fields{ "topic": c.Topic, "channel": c.Channel, }) for message := range c.in { msgLogger := c.logger.WithFields(logrus.Fields{ "message_id": fmt.Sprintf("%s", message.ID), "message_type": msg.Type, "request_id": msg.RequestID, }) ctx := context.WithValue( context.WithValue( context.Background(), "request_id", msg.RequestID, ), "logger", msgLogger, ) // ... c.Handler(ctx, msg.Type, msg.Payload) } }

Slide 13

Slide 13 text

Summary | End-user request | -> Service 1 -> Service 2 -> Service 3 | *- Message bus (topic: stats) | *--> Service 4 *--> Service 5 INFO[TS] log content1 source=service1 request_id: INFO[TS] log content2 source=service2 request_id: INFO[TS] log content3 source=service3 request_id: INFO[TS] log content4 source=service4 request_id: topic=stats channel=ch1 INFO[TS] log content5 source=service5 request_id: topic=stats channel=ch2 To Take away: Formatted logs Request ID Shared Request ID across services

Slide 14

Slide 14 text

Thank you Leo Unbekandt CTO at Scalingo @Soulou (http://twitter.com/Soulou)

Slide 15

Slide 15 text

No content