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(&params); 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 &params
   134  	}
   135  
   136  	// 4. Only appNames is set
   137  	return &params
   138  }