diff --git a/bin/server/main.go b/bin/server/main.go index 9135d2ba..503fdb0c 100644 --- a/bin/server/main.go +++ b/bin/server/main.go @@ -8,6 +8,7 @@ import ( "context" "fmt" + metric "github.com/usememos/memos/plugin/metrics" "github.com/usememos/memos/server" "github.com/usememos/memos/server/profile" "github.com/usememos/memos/store" @@ -34,15 +35,20 @@ func run(profile *profile.Profile) error { return fmt.Errorf("cannot open db: %w", err) } - s := server.NewServer(profile) - + serverInstance := server.NewServer(profile) storeInstance := store.New(db.Db, profile) - s.Store = storeInstance + serverInstance.Store = storeInstance + + metricCollector := server.NewMetricCollector(profile, storeInstance) + serverInstance.Collector = &metricCollector println(greetingBanner) fmt.Printf("Version %s has started at :%d\n", profile.Version, profile.Port) + metricCollector.Collect(ctx, &metric.Metric{ + Name: "servive started", + }) - return s.Run() + return serverInstance.Run() } func execute() error { diff --git a/go.mod b/go.mod index eea5ea7b..5f6ef0e3 100644 --- a/go.mod +++ b/go.mod @@ -33,8 +33,15 @@ require ( github.com/labstack/echo-contrib v0.13.0 ) +require ( + github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/segmentio/backo-go v1.0.1 // indirect + github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect +) + require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/kr/pretty v0.3.1 // indirect + github.com/segmentio/analytics-go v3.1.0+incompatible ) diff --git a/go.sum b/go.sum index fe3cd5f4..dc5c650b 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,8 @@ github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhpl github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/casbin/casbin/v2 v2.51.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -277,6 +279,10 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/segmentio/analytics-go v3.1.0+incompatible h1:IyiOfUgQFVHvsykKKbdI7ZsH374uv3/DfZUo9+G0Z80= +github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48= +github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4= +github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -301,6 +307,8 @@ github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+ github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/plugin/metrics/collector.go b/plugin/metrics/collector.go new file mode 100644 index 00000000..01bde1c8 --- /dev/null +++ b/plugin/metrics/collector.go @@ -0,0 +1,6 @@ +package metric + +// Collector is the interface definition for metric collector. +type Collector interface { + Collect(metric *Metric) error +} diff --git a/plugin/metrics/metric.go b/plugin/metrics/metric.go new file mode 100644 index 00000000..60487016 --- /dev/null +++ b/plugin/metrics/metric.go @@ -0,0 +1,7 @@ +package metric + +// Metric is the API message for metric. +type Metric struct { + Name string + Labels map[string]string +} diff --git a/plugin/metrics/segment/collector.go b/plugin/metrics/segment/collector.go new file mode 100644 index 00000000..f67a66bb --- /dev/null +++ b/plugin/metrics/segment/collector.go @@ -0,0 +1,40 @@ +package segment + +import ( + "time" + + "github.com/google/uuid" + "github.com/segmentio/analytics-go" + metric "github.com/usememos/memos/plugin/metrics" +) + +var _ metric.Collector = (*collector)(nil) + +// collector is the metrics collector https://segment.com/. +type collector struct { + client analytics.Client +} + +// NewCollector creates a new instance of segment. +func NewCollector(key string) metric.Collector { + client := analytics.New(key) + + return &collector{ + client: client, + } +} + +// Collect will exec all the segment collector. +func (c *collector) Collect(metric *metric.Metric) error { + properties := analytics.NewProperties() + for key, value := range metric.Labels { + properties.Set(key, value) + } + + return c.client.Enqueue(analytics.Track{ + Event: string(metric.Name), + AnonymousId: uuid.NewString(), + Properties: properties, + Timestamp: time.Now().UTC(), + }) +} diff --git a/server/auth.go b/server/auth.go index ac2b378e..9eb669da 100644 --- a/server/auth.go +++ b/server/auth.go @@ -7,6 +7,7 @@ import ( "github.com/usememos/memos/api" "github.com/usememos/memos/common" + metric "github.com/usememos/memos/plugin/metrics" "github.com/labstack/echo/v4" "golang.org/x/crypto/bcrypt" @@ -42,6 +43,9 @@ func (s *Server) registerAuthRoutes(g *echo.Group) { if err = setUserSession(c, user); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set signin session").SetInternal(err) } + s.Collector.Collect(ctx, &metric.Metric{ + Name: "user signed in", + }) c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil { @@ -51,10 +55,14 @@ func (s *Server) registerAuthRoutes(g *echo.Group) { }) g.POST("/auth/logout", func(c echo.Context) error { + ctx := c.Request().Context() err := removeUserSession(c) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set logout session").SetInternal(err) } + s.Collector.Collect(ctx, &metric.Metric{ + Name: "user logout", + }) c.Response().WriteHeader(http.StatusOK) return nil @@ -102,6 +110,9 @@ func (s *Server) registerAuthRoutes(g *echo.Group) { if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err) } + s.Collector.Collect(ctx, &metric.Metric{ + Name: "user signed up", + }) err = setUserSession(c, user) if err != nil { diff --git a/server/memo.go b/server/memo.go index e903ba61..6fbd1627 100644 --- a/server/memo.go +++ b/server/memo.go @@ -11,6 +11,7 @@ import ( "github.com/usememos/memos/api" "github.com/usememos/memos/common" + metric "github.com/usememos/memos/plugin/metrics" "github.com/labstack/echo/v4" ) @@ -60,6 +61,9 @@ func (s *Server) registerMemoRoutes(g *echo.Group) { if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create memo").SetInternal(err) } + s.Collector.Collect(ctx, &metric.Metric{ + Name: "memo created", + }) for _, resourceID := range memoCreate.ResourceIDList { if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{ diff --git a/server/metric_collector.go b/server/metric_collector.go new file mode 100644 index 00000000..0be3b368 --- /dev/null +++ b/server/metric_collector.go @@ -0,0 +1,49 @@ +package server + +import ( + "context" + "fmt" + + metric "github.com/usememos/memos/plugin/metrics" + "github.com/usememos/memos/plugin/metrics/segment" + "github.com/usememos/memos/server/profile" + "github.com/usememos/memos/server/version" + "github.com/usememos/memos/store" +) + +// MetricCollector is the metric collector. +type MetricCollector struct { + collector metric.Collector + profile *profile.Profile + store *store.Store +} + +const ( + segmentMetricWriteKey = "FqYUl1CmssHytFSnnVd0efV4gyGeH0dx" +) + +func NewMetricCollector(profile *profile.Profile, store *store.Store) MetricCollector { + c := segment.NewCollector(segmentMetricWriteKey) + + return MetricCollector{ + collector: c, + profile: profile, + store: store, + } +} + +func (mc *MetricCollector) Collect(_ context.Context, metric *metric.Metric) { + if mc.profile.Mode == "dev" { + return + } + + if metric.Labels == nil { + metric.Labels = map[string]string{} + } + metric.Labels["version"] = version.GetCurrentVersion(mc.profile.Mode) + + err := mc.collector.Collect(metric) + if err != nil { + fmt.Printf("Failed to request segment, error: %+v\n", err) + } +} diff --git a/server/resource.go b/server/resource.go index 04f3abe2..2baa7892 100644 --- a/server/resource.go +++ b/server/resource.go @@ -10,6 +10,7 @@ import ( "github.com/usememos/memos/api" "github.com/usememos/memos/common" + metric "github.com/usememos/memos/plugin/metrics" "github.com/labstack/echo/v4" ) @@ -58,6 +59,9 @@ func (s *Server) registerResourceRoutes(g *echo.Group) { if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err) } + s.Collector.Collect(ctx, &metric.Metric{ + Name: "resource created", + }) c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resource)); err != nil { diff --git a/server/server.go b/server/server.go index 351b12ef..19bc31ec 100644 --- a/server/server.go +++ b/server/server.go @@ -17,6 +17,8 @@ import ( type Server struct { e *echo.Echo + Collector *MetricCollector + Profile *profile.Profile Store *store.Store diff --git a/server/shortcut.go b/server/shortcut.go index e672c8ea..7b88f418 100644 --- a/server/shortcut.go +++ b/server/shortcut.go @@ -9,6 +9,7 @@ import ( "github.com/usememos/memos/api" "github.com/usememos/memos/common" + metric "github.com/usememos/memos/plugin/metrics" "github.com/labstack/echo/v4" ) @@ -31,6 +32,9 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) { if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create shortcut").SetInternal(err) } + s.Collector.Collect(ctx, &metric.Metric{ + Name: "shortcut created", + }) c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(shortcut)); err != nil { diff --git a/server/user.go b/server/user.go index 7e70576c..e32a4289 100644 --- a/server/user.go +++ b/server/user.go @@ -9,6 +9,7 @@ import ( "github.com/usememos/memos/api" "github.com/usememos/memos/common" + metric "github.com/usememos/memos/plugin/metrics" "github.com/labstack/echo/v4" "golang.org/x/crypto/bcrypt" @@ -52,6 +53,9 @@ func (s *Server) registerUserRoutes(g *echo.Group) { if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err) } + s.Collector.Collect(ctx, &metric.Metric{ + Name: "user created", + }) c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {