diff --git a/observability.go b/observability.go new file mode 100644 index 0000000..0da65bd --- /dev/null +++ b/observability.go @@ -0,0 +1,13 @@ +package proton + +import ( + "context" + + "github.com/go-resty/resty/v2" +) + +func (c *Client) SendObservabilityBatch(ctx context.Context, req ObservabilityBatch) error { + return c.do(ctx, func(r *resty.Request) (*resty.Response, error) { + return r.SetHeader("Priority", "u=6").SetBody(req).Post("/data/v1/metrics") + }) +} diff --git a/observability_types.go b/observability_types.go new file mode 100644 index 0000000..fbe0ed0 --- /dev/null +++ b/observability_types.go @@ -0,0 +1,12 @@ +package proton + +type ObservabilityBatch struct { + Metrics []ObservabilityMetric `json:"Metrics"` +} + +type ObservabilityMetric struct { + Name string `json:"Name"` + Version int `json:"Version"` + Timestamp int64 `json:"Timestamp"` // Unix timestamp + Data interface{} `json:"Data"` +} diff --git a/server/backend/backend.go b/server/backend/backend.go index 8ce276b..a851bc2 100644 --- a/server/backend/backend.go +++ b/server/backend/backend.go @@ -50,6 +50,8 @@ type unsafeBackend struct { csTicket []string featureFlags []proton.FeatureToggle + + observabilityStatistics ObservabilityStatistics } func readBackendRet[T any](b *Backend, f func(b *unsafeBackend) T) T { @@ -91,16 +93,17 @@ func New(authLife time.Duration, domain string, enableDedup bool) *Backend { return &Backend{ domain: domain, safeBackend: &unsafeBackend{ - accounts: make(map[string]*account), - attachments: make(map[string]*attachment), - attData: make(map[string][]byte), - messages: make(map[string]*message), - labels: make(map[string]*label), - updates: make(map[ID]update), - maxUpdatesPerEvent: 0, - srp: make(map[string]*srp.Server), - authLife: authLife, - enableDedup: enableDedup, + accounts: make(map[string]*account), + attachments: make(map[string]*attachment), + attData: make(map[string][]byte), + messages: make(map[string]*message), + labels: make(map[string]*label), + updates: make(map[ID]update), + maxUpdatesPerEvent: 0, + srp: make(map[string]*srp.Server), + authLife: authLife, + enableDedup: enableDedup, + observabilityStatistics: NewObservabilityStatistics(), }, } } diff --git a/server/backend/observability.go b/server/backend/observability.go new file mode 100644 index 0000000..d7f7704 --- /dev/null +++ b/server/backend/observability.go @@ -0,0 +1,32 @@ +package backend + +import ( + "time" + + "github.com/ProtonMail/go-proton-api" +) + +type ObservabilityStatistics struct { + Metrics []proton.ObservabilityMetric + RequestTime []time.Time +} + +func NewObservabilityStatistics() ObservabilityStatistics { + return ObservabilityStatistics{ + Metrics: make([]proton.ObservabilityMetric, 0), + RequestTime: make([]time.Time, 0), + } +} + +func (b *Backend) PushObservabilityMetrics(metrics []proton.ObservabilityMetric) { + writeBackend(b, func(b *unsafeBackend) { + b.observabilityStatistics.Metrics = append(b.observabilityStatistics.Metrics, metrics...) + b.observabilityStatistics.RequestTime = append(b.observabilityStatistics.RequestTime, time.Now()) + }) +} + +func (b *Backend) GetObservabilityStatistics() ObservabilityStatistics { + return readBackendRet(b, func(b *unsafeBackend) ObservabilityStatistics { + return b.observabilityStatistics + }) +} diff --git a/server/data.go b/server/data.go index 3047b36..139ac5d 100644 --- a/server/data.go +++ b/server/data.go @@ -1,9 +1,10 @@ package server import ( + "net/http" + "github.com/ProtonMail/go-proton-api" "github.com/gin-gonic/gin" - "net/http" ) func (s *Server) handlePostDataStats() gin.HandlerFunc { @@ -51,3 +52,19 @@ func (s *Server) handlePostDataStatsMultiple() gin.HandlerFunc { func validateSendStatReq(req *proton.SendStatsReq) bool { return req.MeasurementGroup != "" } + +func (s *Server) handleObservabilityPost() gin.HandlerFunc { + return func(c *gin.Context) { + var req proton.ObservabilityBatch + if err := c.BindJSON(&req); err != nil { + c.AbortWithStatus(http.StatusBadRequest) + return + } + + s.b.PushObservabilityMetrics(req.Metrics) + + c.JSON(http.StatusOK, gin.H{ + "Code": proton.SuccessCode, + }) + } +} diff --git a/server/router.go b/server/router.go index c149d51..0b14430 100644 --- a/server/router.go +++ b/server/router.go @@ -129,6 +129,8 @@ func initRouter(s *Server) { stats.POST("", s.handlePostDataStats()) stats.POST("/multiple", s.handlePostDataStatsMultiple()) } + // Observability endpoint + data.POST("/metrics", s.handleObservabilityPost()) } // Top level auth routes don't need authentication. diff --git a/server/server.go b/server/server.go index ad516db..29ab61a 100644 --- a/server/server.go +++ b/server/server.go @@ -260,3 +260,7 @@ func (s *Server) PushFeatureFlag(flagName string) { func (s *Server) DeleteFeatureFlags() { s.b.DeleteFeatureFlags() } + +func (s *Server) GetObservabilityStatistics() backend.ObservabilityStatistics { + return s.b.GetObservabilityStatistics() +}