http server

* logging via zap middleware
* basic caching
* base endpoints (requires refactor)
This commit is contained in:
Felipe M 2022-08-14 00:05:42 +02:00
parent f62e5e2f92
commit 5c6f7eebfb
Signed by: fmartingr
GPG Key ID: 716BC147715E716F
7 changed files with 272 additions and 68 deletions

View File

@ -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 {

3
go.mod
View File

@ -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

8
go.sum
View File

@ -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=

View File

@ -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)
},
}),
}
}

View File

@ -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
}
}

View File

@ -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: &notion.DatabaseQueryFilter{
Property: options.DateFieldProperty,
Date: &notion.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
}

View File

@ -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