http server
* logging via zap middleware * basic caching * base endpoints (requires refactor)
This commit is contained in:
parent
f62e5e2f92
commit
5c6f7eebfb
|
@ -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
3
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
|
||||
|
|
8
go.sum
8
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=
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue