github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/api/annotation.go (about) 1 package api 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "net/http" 8 "strconv" 9 "time" 10 11 "github.com/pyroscope-io/pyroscope/pkg/model" 12 "github.com/pyroscope-io/pyroscope/pkg/server/httputils" 13 "github.com/pyroscope-io/pyroscope/pkg/util/attime" 14 "golang.org/x/sync/errgroup" 15 ) 16 17 type AnnotationsService interface { 18 CreateAnnotation(ctx context.Context, params model.CreateAnnotation) (*model.Annotation, error) 19 FindAnnotationsByTimeRange(ctx context.Context, appName string, startTime time.Time, endTime time.Time) ([]model.Annotation, error) 20 } 21 type AnnotationsHandler struct { 22 svc AnnotationsService 23 httpUtils httputils.Utils 24 } 25 26 func NewAnnotationsHandler(svc AnnotationsService, httpUtils httputils.Utils) *AnnotationsHandler { 27 return &AnnotationsHandler{ 28 svc: svc, 29 httpUtils: httpUtils, 30 } 31 } 32 33 type CreateParams struct { 34 AppName string `json:"appName"` 35 AppNames []string `json:"appNames"` 36 Timestamp int64 `json:"timestamp"` 37 Content string `json:"content"` 38 } 39 40 func (h *AnnotationsHandler) CreateAnnotation(w http.ResponseWriter, r *http.Request) { 41 params := h.validateAppNames(w, r) 42 if params == nil { 43 return 44 } 45 46 var timestamp time.Time 47 if params.Timestamp != 0 { 48 timestamp = attime.Parse(strconv.FormatInt(params.Timestamp, 10)) 49 } 50 51 // TODO(eh-am): unify this with render.go 52 type annotationsResponse struct { 53 AppName string `json:"appName"` 54 Content string `json:"content"` 55 Timestamp int64 `json:"timestamp"` 56 } 57 58 createAnnotations := func(ctx context.Context, params *CreateParams) ([]annotationsResponse, error) { 59 g, ctx := errgroup.WithContext(ctx) 60 61 results := make([]annotationsResponse, len(params.AppNames)) 62 for i, appName := range params.AppNames { 63 appName := appName 64 i := i 65 g.Go(func() error { 66 annotation, err := h.svc.CreateAnnotation(ctx, model.CreateAnnotation{ 67 AppName: appName, 68 Timestamp: timestamp, 69 Content: params.Content, 70 }) 71 if err != nil { 72 return err 73 } 74 75 results[i] = annotationsResponse{ 76 AppName: annotation.AppName, 77 Content: annotation.Content, 78 Timestamp: annotation.Timestamp.Unix(), 79 } 80 81 return err 82 }) 83 } 84 85 if err := g.Wait(); err != nil { 86 return nil, err 87 } 88 return results, nil 89 } 90 91 res, err := createAnnotations(r.Context(), params) 92 if err != nil { 93 h.httpUtils.HandleError(r, w, err) 94 return 95 } 96 97 w.WriteHeader(http.StatusCreated) 98 if len(res) == 1 { 99 h.httpUtils.WriteResponseJSON(r, w, res[0]) 100 return 101 } 102 103 h.httpUtils.WriteResponseJSON(r, w, res) 104 } 105 106 // validateAppNames handles the different combinations between (`appName` and `appNames`) 107 // in the failure case, it returns nil and serves an error 108 // in the success case, it returns a `CreateParams` struct where `appNames` is ALWAYS populated with at least one appName 109 func (h *AnnotationsHandler) validateAppNames(w http.ResponseWriter, r *http.Request) *CreateParams { 110 var params CreateParams 111 112 if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { 113 h.httpUtils.WriteInternalServerError(r, w, err, "failed to unmarshal JSON") 114 return nil 115 } 116 117 // handling `appName` and `appNames` 118 // 1. Both are set, is invalid 119 if params.AppName != "" && len(params.AppNames) > 0 { 120 h.httpUtils.HandleError(r, w, model.ValidationError{errors.New("only one of 'appName' and 'appNames' can be specified")}) 121 return nil 122 } 123 124 // 2. None are set 125 if params.AppName == "" && len(params.AppNames) <= 0 { 126 h.httpUtils.HandleError(r, w, model.ValidationError{errors.New("at least one of 'appName' and 'appNames' needs to be specified")}) 127 return nil 128 } 129 130 // 3. Only appName is set 131 if params.AppName != "" && len(params.AppNames) <= 0 { 132 params.AppNames = append(params.AppNames, params.AppName) 133 return ¶ms 134 } 135 136 // 4. Only appNames is set 137 return ¶ms 138 }