From 5c6f7eebfb5183f259137cd0d1c76f5ea376c2fa Mon Sep 17 00:00:00 2001 From: Felipe M Date: Sun, 14 Aug 2022 00:05:42 +0200 Subject: [PATCH] http server * logging via zap middleware * basic caching * base endpoints (requires refactor) --- cmd/notion2ical/main.go | 8 +- go.mod | 3 + go.sum | 8 ++ internal/server/http.go | 62 ---------- internal/server/http/middleware/zap.go | 91 +++++++++++++++ internal/server/http/server.go | 156 +++++++++++++++++++++++++ internal/server/server.go | 12 +- 7 files changed, 272 insertions(+), 68 deletions(-) delete mode 100644 internal/server/http.go create mode 100644 internal/server/http/middleware/zap.go create mode 100644 internal/server/http/server.go diff --git a/cmd/notion2ical/main.go b/cmd/notion2ical/main.go index 1695c66..96b0883 100644 --- a/cmd/notion2ical/main.go +++ b/cmd/notion2ical/main.go @@ -3,6 +3,7 @@ package main import ( "context" + "github.com/fmartingr/notion2ical/internal/notion" "github.com/fmartingr/notion2ical/internal/server" "go.uber.org/zap" ) @@ -22,9 +23,14 @@ func main() { } }() + config := server.ParseServerConfiguration(ctx, logger) + + notionClient := notion.NewNotionClient(config.Notion.IntegrationToken) + server := server.NewServer( logger, - server.ParseServerConfiguration(ctx, logger), + config, + notionClient, ) if err := server.Start(ctx); err != nil { diff --git a/go.mod b/go.mod index 86b0bca..680b0b3 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/fmartingr/notion2ical go 1.19 require ( + github.com/dstotijn/go-notion v0.6.1 + github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f github.com/gofiber/fiber/v2 v2.36.0 github.com/sethvargo/go-envconfig v0.8.2 go.uber.org/zap v1.22.0 @@ -11,6 +13,7 @@ require ( require ( github.com/andybalholm/brotli v1.0.4 // indirect github.com/klauspost/compress v1.15.0 // indirect + github.com/teambition/rrule-go v1.7.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.38.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect diff --git a/go.sum b/go.sum index 0b8bca7..bf9063a 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,13 @@ github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLj github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dstotijn/go-notion v0.6.1 h1:gmwU/JCdLC5szMasfysDOm8UG6/3P0bTUe0+CeW2fmI= +github.com/dstotijn/go-notion v0.6.1/go.mod h1:oxd+T9Wxduj5ZN7MRiHWtyGhGZLUFsUpZHMLS4uI1Qc= +github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f h1:feGUUxxvOtWVOhTko8Cbmp33a+tU0IMZxMEmnkoAISQ= +github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f/go.mod h1:2MKFUgfNMULRxqZkadG1Vh44we3y5gJAtTBlVsx1BKQ= github.com/gofiber/fiber/v2 v2.36.0 h1:1qLMe5rhXFLPa2SjK10Wz7WFgLwYi4TYg7XrjztJHqA= github.com/gofiber/fiber/v2 v2.36.0/go.mod h1:tgCr+lierLwLoVHHO/jn3Niannv34WRkQETU8wiL9fQ= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= @@ -17,6 +22,8 @@ github.com/sethvargo/go-envconfig v0.8.2/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YI github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/teambition/rrule-go v1.7.2 h1:goEajFWYydfCgavn2m/3w5U+1b3PGqPUHx/fFSVfTy0= +github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.38.0 h1:yTjSSNjuDi2PPvXY2836bIwLmiTS2T4T9p1coQshpco= @@ -44,4 +51,5 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/server/http.go b/internal/server/http.go deleted file mode 100644 index 700f81f..0000000 --- a/internal/server/http.go +++ /dev/null @@ -1,62 +0,0 @@ -package server - -import ( - "context" - "fmt" - - "github.com/gofiber/fiber/v2" - "go.uber.org/zap" -) - -type httpServer struct { - http *fiber.App - addr string - - logger *zap.Logger -} - -func (s *httpServer) Start(_ context.Context) error { - s.http. - Static("/", "./public"). - Get("/calendar", s.calendarHandler). - Get("/liveness", s.livenessHandler). - Use(s.notFound) - - s.logger.Info("starting http server", zap.String("addr", s.addr)) - return s.http.Listen(s.addr) -} - -func (s *httpServer) Stop(ctx context.Context) error { - s.logger.Info("stoppping http server") - return s.http.Shutdown() -} - -func (s *httpServer) notFound(c *fiber.Ctx) error { - return c.SendStatus(404) -} - -func (s *httpServer) livenessHandler(c *fiber.Ctx) error { - return c.SendStatus(200) -} - -func (s *httpServer) calendarHandler(c *fiber.Ctx) error { - return c.SendStatus(501) -} - -func NewHttpServer(logger *zap.Logger, port int) *httpServer { - return &httpServer{ - logger: logger, - addr: fmt.Sprintf(":%d", port), - http: fiber.New(fiber.Config{ - ErrorHandler: func(c *fiber.Ctx, err error) error { - logger.Error( - "handler error", - zap.String("method", c.Method()), - zap.String("path", c.Path()), - zap.Error(err), - ) - return c.SendStatus(500) - }, - }), - } -} diff --git a/internal/server/http/middleware/zap.go b/internal/server/http/middleware/zap.go new file mode 100644 index 0000000..402ca80 --- /dev/null +++ b/internal/server/http/middleware/zap.go @@ -0,0 +1,91 @@ +// Fiber middleware to enable zap logger for each request +// Adapted from https://gl.oddhunters.com/pub/fiberzap +package middleware + +import ( + "os" + "strconv" + "sync" + "time" + + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" +) + +// ZapMiddlewareConfig defines the config for middleware +type ZapMiddlewareConfig struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Logger defines zap logger instance + Logger *zap.Logger + + // CacheHeader defines the header name to get cache status from + CacheHeader string +} + +// New creates a new middleware handler +func NewZapMiddleware(config ZapMiddlewareConfig) fiber.Handler { + var ( + errPadding = 15 + start, stop time.Time + once sync.Once + errHandler fiber.ErrorHandler + ) + + return func(c *fiber.Ctx) error { + if config.Next != nil && config.Next(c) { + return c.Next() + } + + once.Do(func() { + errHandler = c.App().Config().ErrorHandler + stack := c.App().Stack() + for m := range stack { + for r := range stack[m] { + if len(stack[m][r].Path) > errPadding { + errPadding = len(stack[m][r].Path) + } + } + } + }) + + start = time.Now() + + chainErr := c.Next() + + if chainErr != nil { + if err := errHandler(c, chainErr); err != nil { + _ = c.SendStatus(fiber.StatusInternalServerError) + } + } + + stop = time.Now() + + fields := []zap.Field{ + zap.Namespace("context"), + zap.String("method", c.Method()), + zap.String("path", c.Path()), + zap.Int("status_code", c.Response().StatusCode()), + zap.String("pid", strconv.Itoa(os.Getpid())), + zap.String("time", stop.Sub(start).String()), + zap.String("cache", string(c.Response().Header.Peek(config.CacheHeader))), + zap.String("request-id", c.Locals("requestid").(string)), + } + + formatErr := "" + if chainErr != nil { + formatErr = chainErr.Error() + fields = append(fields, zap.String("error", formatErr)) + config.Logger.With(fields...).Error(formatErr) + + return nil + } + + config.Logger.With(fields...).Info("request handled") + + return nil + } +} diff --git a/internal/server/http/server.go b/internal/server/http/server.go new file mode 100644 index 0000000..402ba6a --- /dev/null +++ b/internal/server/http/server.go @@ -0,0 +1,156 @@ +package http + +import ( + "bytes" + "context" + "fmt" + "time" + + "github.com/dstotijn/go-notion" + "github.com/emersion/go-ical" + notionClient "github.com/fmartingr/notion2ical/internal/notion" + "github.com/fmartingr/notion2ical/internal/server/http/middleware" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cache" + "github.com/gofiber/fiber/v2/middleware/recover" + "github.com/gofiber/fiber/v2/middleware/requestid" + "github.com/gofiber/fiber/v2/middleware/timeout" + "github.com/gofiber/fiber/v2/utils" + "go.uber.org/zap" +) + +type HttpServer struct { + http *fiber.App + addr string + logger *zap.Logger + notion *notionClient.NotionClient +} + +func (s *HttpServer) Setup() { + s.http. + Use(requestid.New(requestid.Config{ + Generator: utils.UUIDv4, + })). + Use(middleware.NewZapMiddleware(middleware.ZapMiddlewareConfig{ + Logger: s.logger, + CacheHeader: "X-Cache", + })). + Use(cache.New(cache.Config{ + Next: func(c *fiber.Ctx) bool { + return c.Query("refresh") == "true" + }, + Expiration: 60 * time.Minute, + CacheControl: true, + CacheHeader: "X-Cache", + // KeyGenerator: func(c *fiber.Ctx) string { + // return utils.CopyString(c.Path() + string(c.Context().QueryArgs().QueryString())) + // }, + })). + Use(recover.New()). + Static("/", "./public"). + Get("/calendar/:databaseID", timeout.New(s.calendarHandler, time.Second*30)). + Get("/liveness", s.livenessHandler). + Use(s.notFound) +} + +func (s *HttpServer) Start(_ context.Context) error { + s.logger.Info("starting http server", zap.String("addr", s.addr)) + return s.http.Listen(s.addr) +} + +func (s *HttpServer) Stop(ctx context.Context) error { + s.logger.Info("stoppping http server") + return s.http.Shutdown() +} + +func (s *HttpServer) notFound(c *fiber.Ctx) error { + return c.SendStatus(404) +} + +func (s *HttpServer) livenessHandler(c *fiber.Ctx) error { + return c.SendStatus(200) +} + +type calendarOptions struct { + AllDayEvents bool `query:"all_day_events"` + DateFieldProperty string `query:"date_field_property"` + NameProperty string `query:"name_property"` +} + +func (s *HttpServer) calendarHandler(c *fiber.Ctx) error { + var options calendarOptions + if err := c.QueryParser(&options); err != nil { + s.logger.Error("error parsing query", zap.String("query", c.Context().QueryArgs().String())) + } + + dbID := c.Params("databaseID") + + query := notion.DatabaseQuery{ + Filter: ¬ion.DatabaseQueryFilter{ + Property: options.DateFieldProperty, + Date: ¬ion.DateDatabaseQueryFilter{ + IsNotEmpty: true, + }, + }, + } + + result, err := s.notion.Client.QueryDatabase(c.Context(), dbID, &query) + if err != nil { + s.logger.Error("can't query notion database", zap.Error(err)) + return c.SendStatus(500) + } + + cal := ical.NewCalendar() + cal.Props.SetText(ical.PropVersion, "2.0") + cal.Props.SetText(ical.PropProductID, "-//notion2ical.fmartingr.dev//NONSGML PDA Calendar Version 1.0//EN") + + for _, r := range result.Results { + event := ical.NewEvent() + event.Props.SetText(ical.PropUID, r.ID) + dateProperty := r.Properties.(notion.DatabasePageProperties)[options.DateFieldProperty].Date + dateStart := dateProperty.Start.Time + dateEnd := dateStart + if dateProperty.End != nil { + dateEnd = dateProperty.End.Time + } + if options.AllDayEvents { + event.Props.SetDate(ical.PropDateTimeStamp, dateStart) + event.Props.SetDate(ical.PropDateTimeStart, dateEnd) + } else { + event.Props.SetDateTime(ical.PropDateTimeStamp, dateStart) + event.Props.SetDateTime(ical.PropDateTimeStart, dateEnd) + } + event.Props.SetText(ical.PropSummary, r.Properties.(notion.DatabasePageProperties)[options.NameProperty].Title[0].Text.Content) + + cal.Children = append(cal.Children, event.Component) + } + + var buf bytes.Buffer + if err := ical.NewEncoder(&buf).Encode(cal); err != nil { + s.logger.Fatal("error encoding calendar", zap.Error(err)) + } + + return c.Send(buf.Bytes()) +} + +func NewHttpServer(logger *zap.Logger, port int, notionClient *notionClient.NotionClient) *HttpServer { + server := HttpServer{ + logger: logger, + addr: fmt.Sprintf(":%d", port), + notion: notionClient, + http: fiber.New(fiber.Config{ + ErrorHandler: func(c *fiber.Ctx, err error) error { + logger.Error( + "handler error", + zap.String("method", c.Method()), + zap.String("path", c.Path()), + zap.Error(err), + ) + return c.SendStatus(500) + }, + }), + } + server.Setup() + + return &server +} diff --git a/internal/server/server.go b/internal/server/server.go index 8503ddc..133ddec 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -3,13 +3,15 @@ package server import ( "context" "errors" - "net/http" + stdlibHttp "net/http" "os" "os/signal" "syscall" "time" internalModels "github.com/fmartingr/notion2ical/internal/models" + "github.com/fmartingr/notion2ical/internal/notion" + "github.com/fmartingr/notion2ical/internal/server/http" "go.uber.org/zap" ) @@ -27,7 +29,7 @@ func (s *Server) Start(ctx context.Context) error { if s.config.Http.Enabled { go func() { - if err := s.Http.Start(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) { + if err := s.Http.Start(ctx); err != nil && !errors.Is(err, stdlibHttp.ErrServerClosed) { s.logger.Fatal("error starting server", zap.Error(err)) } }() @@ -53,19 +55,19 @@ func (s *Server) Stop() { defer cancel() if s.config.Http.Enabled { - if err := s.Http.Stop(shuwdownContext); err != nil && !errors.Is(err, http.ErrServerClosed) { + if err := s.Http.Stop(shuwdownContext); err != nil && !errors.Is(err, stdlibHttp.ErrServerClosed) { s.logger.Fatal("error shutting down http server", zap.Error(err)) } } } -func NewServer(logger *zap.Logger, conf *ServerConfig) *Server { +func NewServer(logger *zap.Logger, conf *ServerConfig, notionClient *notion.NotionClient) *Server { server := &Server{ logger: logger, config: conf, } if conf.Http.Enabled { - server.Http = NewHttpServer(logger, conf.Http.Port) + server.Http = http.NewHttpServer(logger, conf.Http.Port, notionClient) } return server