github.com/influxdata/influxdb/v2@v2.7.6/annotation.go (about)

     1  package influxdb
     2  
     3  import (
     4  	"context"
     5  	"database/sql/driver"
     6  	"encoding/json"
     7  	"fmt"
     8  	"regexp"
     9  	"strings"
    10  	"time"
    11  	"unicode/utf8"
    12  
    13  	"github.com/influxdata/influxdb/v2/kit/platform"
    14  	"github.com/influxdata/influxdb/v2/kit/platform/errors"
    15  )
    16  
    17  var (
    18  	errEmptySummary = &errors.Error{
    19  		Code: errors.EInvalid,
    20  		Msg:  "summary cannot be empty",
    21  	}
    22  	errSummaryTooLong = &errors.Error{
    23  		Code: errors.EInvalid,
    24  		Msg:  "summary must be less than 255 characters",
    25  	}
    26  	errStreamTagTooLong = &errors.Error{
    27  		Code: errors.EInvalid,
    28  		Msg:  "stream tag must be less than 255 characters",
    29  	}
    30  	errStreamNameTooLong = &errors.Error{
    31  		Code: errors.EInvalid,
    32  		Msg:  "stream name must be less than 255 characters",
    33  	}
    34  	errStreamDescTooLong = &errors.Error{
    35  		Code: errors.EInvalid,
    36  		Msg:  "stream description must be less than 1024 characters",
    37  	}
    38  	errStickerTooLong = &errors.Error{
    39  		Code: errors.EInvalid,
    40  		Msg:  "stickers must be less than 255 characters",
    41  	}
    42  	errMsgTooLong = &errors.Error{
    43  		Code: errors.EInvalid,
    44  		Msg:  "message must be less than 4096 characters",
    45  	}
    46  	errReversedTimes = &errors.Error{
    47  		Code: errors.EInvalid,
    48  		Msg:  "start time must come before end time",
    49  	}
    50  	errMissingStreamName = &errors.Error{
    51  		Code: errors.EInvalid,
    52  		Msg:  "stream name must be set",
    53  	}
    54  	errMissingStreamTagOrId = &errors.Error{
    55  		Code: errors.EInvalid,
    56  		Msg:  "stream tag or id must be set",
    57  	}
    58  	errMissingEndTime = &errors.Error{
    59  		Code: errors.EInvalid,
    60  		Msg:  "end time must be set",
    61  	}
    62  	errMissingStartTime = &errors.Error{
    63  		Code: errors.EInvalid,
    64  		Msg:  "start time must be set",
    65  	}
    66  )
    67  
    68  func invalidStickerError(s string) error {
    69  	return &errors.Error{
    70  		Code: errors.EInternal,
    71  		Msg:  fmt.Sprintf("invalid sticker: %q", s),
    72  	}
    73  }
    74  
    75  func stickerSliceToMap(stickers []string) (map[string]string, error) {
    76  	stickerMap := map[string]string{}
    77  
    78  	for i := range stickers {
    79  		if stick0, stick1, found := strings.Cut(stickers[i], "="); found {
    80  			stickerMap[stick0] = stick1
    81  		} else {
    82  			return nil, invalidStickerError(stickers[i])
    83  		}
    84  	}
    85  
    86  	return stickerMap, nil
    87  }
    88  
    89  // AnnotationService is the service contract for Annotations
    90  type AnnotationService interface {
    91  	// CreateAnnotations creates annotations.
    92  	CreateAnnotations(ctx context.Context, orgID platform.ID, create []AnnotationCreate) ([]AnnotationEvent, error)
    93  	// ListAnnotations lists all annotations matching the filter.
    94  	ListAnnotations(ctx context.Context, orgID platform.ID, filter AnnotationListFilter) ([]StoredAnnotation, error)
    95  	// GetAnnotation gets an annotation by id.
    96  	GetAnnotation(ctx context.Context, id platform.ID) (*StoredAnnotation, error)
    97  	// DeleteAnnotations deletes annotations matching the filter.
    98  	DeleteAnnotations(ctx context.Context, orgID platform.ID, delete AnnotationDeleteFilter) error
    99  	// DeleteAnnotation deletes an annotation by id.
   100  	DeleteAnnotation(ctx context.Context, id platform.ID) error
   101  	// UpdateAnnotation updates an annotation.
   102  	UpdateAnnotation(ctx context.Context, id platform.ID, update AnnotationCreate) (*AnnotationEvent, error)
   103  
   104  	// ListStreams lists all streams matching the filter.
   105  	ListStreams(ctx context.Context, orgID platform.ID, filter StreamListFilter) ([]StoredStream, error)
   106  	// CreateOrUpdateStream creates or updates the matching stream by name.
   107  	CreateOrUpdateStream(ctx context.Context, orgID platform.ID, stream Stream) (*ReadStream, error)
   108  	// GetStream gets a stream by id. Currently this is only used for authorization, and there are no
   109  	// API routes for getting a single stream by ID.
   110  	GetStream(ctx context.Context, id platform.ID) (*StoredStream, error)
   111  	// UpdateStream updates the stream by the ID.
   112  	UpdateStream(ctx context.Context, id platform.ID, stream Stream) (*ReadStream, error)
   113  	// DeleteStreams deletes one or more streams by name.
   114  	DeleteStreams(ctx context.Context, orgID platform.ID, delete BasicStream) error
   115  	// DeleteStreamByID deletes the stream metadata by id.
   116  	DeleteStreamByID(ctx context.Context, id platform.ID) error
   117  }
   118  
   119  // AnnotationEvent contains fields for annotating an event.
   120  type AnnotationEvent struct {
   121  	ID               platform.ID `json:"id,omitempty"` // ID is the annotation ID.
   122  	AnnotationCreate             // AnnotationCreate defines the common input/output bits of an annotation.
   123  }
   124  
   125  // AnnotationCreate contains user providable fields for annotating an event.
   126  type AnnotationCreate struct {
   127  	StreamTag string             `json:"stream,omitempty"`    // StreamTag provides a means to logically group a set of annotated events.
   128  	Summary   string             `json:"summary"`             // Summary is the only field required to annotate an event.
   129  	Message   string             `json:"message,omitempty"`   // Message provides more details about the event being annotated.
   130  	Stickers  AnnotationStickers `json:"stickers,omitempty"`  // Stickers are like tags, but named something obscure to differentiate them from influx tags. They are there to differentiate an annotated event.
   131  	EndTime   *time.Time         `json:"endTime,omitempty"`   // EndTime is the time of the event being annotated. Defaults to now if not set.
   132  	StartTime *time.Time         `json:"startTime,omitempty"` // StartTime is the start time of the event being annotated. Defaults to EndTime if not set.
   133  }
   134  
   135  // StoredAnnotation represents annotation data to be stored in the database.
   136  type StoredAnnotation struct {
   137  	ID        platform.ID        `db:"id"`        // ID is the annotation's id.
   138  	OrgID     platform.ID        `db:"org_id"`    // OrgID is the annotations's owning organization.
   139  	StreamID  platform.ID        `db:"stream_id"` // StreamID is the id of a stream.
   140  	StreamTag string             `db:"stream"`    // StreamTag is the name of a stream (when selecting with join of streams).
   141  	Summary   string             `db:"summary"`   // Summary is the summary of the annotated event.
   142  	Message   string             `db:"message"`   // Message is a longer description of the annotated event.
   143  	Stickers  AnnotationStickers `db:"stickers"`  // Stickers are additional labels to group annotations by.
   144  	Duration  string             `db:"duration"`  // Duration is the time range (with zone) of an annotated event.
   145  	Lower     string             `db:"lower"`     // Lower is the time an annotated event begins.
   146  	Upper     string             `db:"upper"`     // Upper is the time an annotated event ends.
   147  }
   148  
   149  // ToCreate is a utility method for converting a StoredAnnotation to an AnnotationCreate type
   150  func (s StoredAnnotation) ToCreate() (*AnnotationCreate, error) {
   151  	et, err := time.Parse(time.RFC3339Nano, s.Upper)
   152  	if err != nil {
   153  		return nil, err
   154  	}
   155  
   156  	st, err := time.Parse(time.RFC3339Nano, s.Lower)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  
   161  	return &AnnotationCreate{
   162  		StreamTag: s.StreamTag,
   163  		Summary:   s.Summary,
   164  		Message:   s.Message,
   165  		Stickers:  s.Stickers,
   166  		EndTime:   &et,
   167  		StartTime: &st,
   168  	}, nil
   169  }
   170  
   171  // ToEvent is a utility method for converting a StoredAnnotation to an AnnotationEvent type
   172  func (s StoredAnnotation) ToEvent() (*AnnotationEvent, error) {
   173  	c, err := s.ToCreate()
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	return &AnnotationEvent{
   179  		ID:               s.ID,
   180  		AnnotationCreate: *c,
   181  	}, nil
   182  }
   183  
   184  type AnnotationStickers map[string]string
   185  
   186  // Value implements the database/sql Valuer interface for adding AnnotationStickers to the database
   187  // Stickers are stored in the database as a slice of strings like "[key=val]"
   188  // They are encoded into a JSON string for storing into the database, and the JSON sqlite extension is
   189  // able to manipulate them like an object.
   190  func (a AnnotationStickers) Value() (driver.Value, error) {
   191  	stickSlice := make([]string, 0, len(a))
   192  
   193  	for k, v := range a {
   194  		stickSlice = append(stickSlice, fmt.Sprintf("%s=%s", k, v))
   195  	}
   196  
   197  	sticks, err := json.Marshal(stickSlice)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  
   202  	return string(sticks), nil
   203  }
   204  
   205  // Scan implements the database/sql Scanner interface for retrieving AnnotationStickers from the database
   206  // The string is decoded into a slice of strings, which are then converted back into a map
   207  func (a *AnnotationStickers) Scan(value interface{}) error {
   208  	vString, ok := value.(string)
   209  	if !ok {
   210  		return &errors.Error{
   211  			Code: errors.EInternal,
   212  			Msg:  "could not load stickers from sqlite",
   213  		}
   214  	}
   215  
   216  	var stickSlice []string
   217  	if err := json.NewDecoder(strings.NewReader(vString)).Decode(&stickSlice); err != nil {
   218  		return err
   219  	}
   220  
   221  	stickMap, err := stickerSliceToMap(stickSlice)
   222  	if err != nil {
   223  		return nil
   224  	}
   225  
   226  	*a = stickMap
   227  	return nil
   228  }
   229  
   230  // Validate validates the creation object.
   231  func (a *AnnotationCreate) Validate(nowFunc func() time.Time) error {
   232  	switch s := utf8.RuneCountInString(a.Summary); {
   233  	case s <= 0:
   234  		return errEmptySummary
   235  	case s > 255:
   236  		return errSummaryTooLong
   237  	}
   238  
   239  	switch t := utf8.RuneCountInString(a.StreamTag); {
   240  	case t == 0:
   241  		a.StreamTag = "default"
   242  	case t > 255:
   243  		return errStreamTagTooLong
   244  	}
   245  
   246  	if utf8.RuneCountInString(a.Message) > 4096 {
   247  		return errMsgTooLong
   248  	}
   249  
   250  	for k, v := range a.Stickers {
   251  		if utf8.RuneCountInString(k) > 255 || utf8.RuneCountInString(v) > 255 {
   252  			return errStickerTooLong
   253  		}
   254  	}
   255  
   256  	now := nowFunc()
   257  	if a.EndTime == nil {
   258  		a.EndTime = &now
   259  	}
   260  
   261  	if a.StartTime == nil {
   262  		a.StartTime = a.EndTime
   263  	}
   264  
   265  	if a.EndTime.Before(*(a.StartTime)) {
   266  		return errReversedTimes
   267  	}
   268  
   269  	return nil
   270  }
   271  
   272  // AnnotationDeleteFilter contains fields for deleting an annotated event.
   273  type AnnotationDeleteFilter struct {
   274  	StreamTag string            `json:"stream,omitempty"`    // StreamTag provides a means to logically group a set of annotated events.
   275  	StreamID  platform.ID       `json:"streamID,omitempty"`  // StreamID provides a means to logically group a set of annotated events.
   276  	Stickers  map[string]string `json:"stickers,omitempty"`  // Stickers are like tags, but named something obscure to differentiate them from influx tags. They are there to differentiate an annotated event.
   277  	EndTime   *time.Time        `json:"endTime,omitempty"`   // EndTime is the time of the event being annotated. Defaults to now if not set.
   278  	StartTime *time.Time        `json:"startTime,omitempty"` // StartTime is the start time of the event being annotated. Defaults to EndTime if not set.
   279  }
   280  
   281  // Validate validates the deletion object.
   282  func (a *AnnotationDeleteFilter) Validate() error {
   283  	var errs []string
   284  
   285  	if len(a.StreamTag) == 0 && !a.StreamID.Valid() {
   286  		errs = append(errs, errMissingStreamTagOrId.Error())
   287  	}
   288  
   289  	if a.EndTime == nil {
   290  		errs = append(errs, errMissingEndTime.Error())
   291  	}
   292  
   293  	if a.StartTime == nil {
   294  		errs = append(errs, errMissingStartTime.Error())
   295  	}
   296  
   297  	if len(errs) > 0 {
   298  		return &errors.Error{
   299  			Code: errors.EInvalid,
   300  			Msg:  strings.Join(errs, "; "),
   301  		}
   302  	}
   303  
   304  	if a.EndTime.Before(*(a.StartTime)) {
   305  		return errReversedTimes
   306  	}
   307  
   308  	return nil
   309  }
   310  
   311  var dre = regexp.MustCompile(`stickers\[(.*)\]`)
   312  
   313  // SetStickers sets the stickers from the query parameters.
   314  func (a *AnnotationDeleteFilter) SetStickers(vals map[string][]string) {
   315  	if a.Stickers == nil {
   316  		a.Stickers = map[string]string{}
   317  	}
   318  
   319  	for k, v := range vals {
   320  		if ss := dre.FindStringSubmatch(k); len(ss) == 2 && len(v) > 0 {
   321  			a.Stickers[ss[1]] = v[0]
   322  		}
   323  	}
   324  }
   325  
   326  // AnnotationList defines the structure of the response when listing annotations.
   327  type AnnotationList struct {
   328  	StreamTag   string           `json:"stream"`
   329  	Annotations []ReadAnnotation `json:"annotations"`
   330  }
   331  
   332  // ReadAnnotations allows annotations to be assigned to a stream.
   333  type ReadAnnotations map[string][]ReadAnnotation
   334  
   335  // MarshalJSON allows us to marshal the annotations belonging to a stream properly.
   336  func (s ReadAnnotations) MarshalJSON() ([]byte, error) {
   337  	annotationList := []AnnotationList{}
   338  
   339  	for k, v := range s {
   340  		annotationList = append(annotationList, AnnotationList{
   341  			StreamTag:   k,
   342  			Annotations: v,
   343  		})
   344  	}
   345  
   346  	return json.Marshal(annotationList)
   347  }
   348  
   349  // ReadAnnotation defines the simplest form of an annotation to be returned. Essentially, it's AnnotationEvent without stream info.
   350  type ReadAnnotation struct {
   351  	ID        platform.ID       `json:"id"`                  // ID is the annotation's generated id.
   352  	Summary   string            `json:"summary"`             // Summary is the only field required to annotate an event.
   353  	Message   string            `json:"message,omitempty"`   // Message provides more details about the event being annotated.
   354  	Stickers  map[string]string `json:"stickers,omitempty"`  // Stickers are like tags, but named something obscure to differentiate them from influx tags. They are there to differentiate an annotated event.
   355  	EndTime   string            `json:"endTime"`             // EndTime is the time of the event being annotated.
   356  	StartTime string            `json:"startTime,omitempty"` // StartTime is the start time of the event being annotated.
   357  }
   358  
   359  // AnnotationListFilter is a selection filter for listing annotations.
   360  type AnnotationListFilter struct {
   361  	StickerIncludes AnnotationStickers `json:"stickerIncludes,omitempty"` // StickerIncludes allows the user to filter annotated events based on it's sticker.
   362  	StreamIncludes  []string           `json:"streamIncludes,omitempty"`  // StreamIncludes allows the user to filter annotated events by stream.
   363  	BasicFilter
   364  }
   365  
   366  // Validate validates the filter.
   367  func (f *AnnotationListFilter) Validate(nowFunc func() time.Time) error {
   368  	return f.BasicFilter.Validate(nowFunc)
   369  }
   370  
   371  var re = regexp.MustCompile(`stickerIncludes\[(.*)\]`)
   372  
   373  // SetStickerIncludes sets the stickerIncludes from the query parameters.
   374  func (f *AnnotationListFilter) SetStickerIncludes(vals map[string][]string) {
   375  	if f.StickerIncludes == nil {
   376  		f.StickerIncludes = map[string]string{}
   377  	}
   378  
   379  	for k, v := range vals {
   380  		if ss := re.FindStringSubmatch(k); len(ss) == 2 && len(v) > 0 {
   381  			f.StickerIncludes[ss[1]] = v[0]
   382  		}
   383  	}
   384  }
   385  
   386  // StreamListFilter is a selection filter for listing streams. Streams are not considered first class resources, but depend on an annotation using them.
   387  type StreamListFilter struct {
   388  	StreamIncludes []string `json:"streamIncludes,omitempty"` // StreamIncludes allows the user to filter streams returned.
   389  	BasicFilter
   390  }
   391  
   392  // Validate validates the filter.
   393  func (f *StreamListFilter) Validate(nowFunc func() time.Time) error {
   394  	return f.BasicFilter.Validate(nowFunc)
   395  }
   396  
   397  // Stream defines the stream metadata. Used in create and update requests/responses. Delete requests will only require stream name.
   398  type Stream struct {
   399  	Name        string `json:"stream"`                // Name is the name of a stream.
   400  	Description string `json:"description,omitempty"` // Description is more information about a stream.
   401  }
   402  
   403  // ReadStream defines the returned stream.
   404  type ReadStream struct {
   405  	ID          platform.ID `json:"id" db:"id"`                             // ID is the id of a stream.
   406  	Name        string      `json:"stream" db:"name"`                       // Name is the name of a stream.
   407  	Description string      `json:"description,omitempty" db:"description"` // Description is more information about a stream.
   408  	CreatedAt   time.Time   `json:"createdAt" db:"created_at"`              // CreatedAt is a timestamp.
   409  	UpdatedAt   time.Time   `json:"updatedAt" db:"updated_at"`              // UpdatedAt is a timestamp.
   410  }
   411  
   412  // IsValid validates the stream.
   413  func (s *Stream) Validate(strict bool) error {
   414  	switch nameChars := utf8.RuneCountInString(s.Name); {
   415  	case nameChars <= 0:
   416  		if strict {
   417  			return errMissingStreamName
   418  		}
   419  		s.Name = "default"
   420  	case nameChars > 255:
   421  		return errStreamNameTooLong
   422  	}
   423  
   424  	if utf8.RuneCountInString(s.Description) > 1024 {
   425  		return errStreamDescTooLong
   426  	}
   427  
   428  	return nil
   429  }
   430  
   431  // StoredStream represents stream data to be stored in the metadata database.
   432  type StoredStream struct {
   433  	ID          platform.ID `db:"id"`          // ID is the stream's id.
   434  	OrgID       platform.ID `db:"org_id"`      // OrgID is the stream's owning organization.
   435  	Name        string      `db:"name"`        // Name is the name of a stream.
   436  	Description string      `db:"description"` // Description is more information about a stream.
   437  	CreatedAt   time.Time   `db:"created_at"`  // CreatedAt is a timestamp.
   438  	UpdatedAt   time.Time   `db:"updated_at"`  // UpdatedAt is a timestamp.
   439  }
   440  
   441  // BasicStream defines a stream by name. Used for stream deletes.
   442  type BasicStream struct {
   443  	Names []string `json:"stream"`
   444  }
   445  
   446  // IsValid validates the stream is not empty.
   447  func (s BasicStream) IsValid() bool {
   448  	if len(s.Names) <= 0 {
   449  		return false
   450  	}
   451  
   452  	for i := range s.Names {
   453  		if len(s.Names[i]) <= 0 {
   454  			return false
   455  		}
   456  	}
   457  
   458  	return true
   459  }
   460  
   461  // BasicFilter defines common filter options.
   462  type BasicFilter struct {
   463  	StartTime *time.Time `json:"startTime,omitempty"` // StartTime is the time the event being annotated started.
   464  	EndTime   *time.Time `json:"endTime,omitempty"`   // EndTime is the time the event being annotated ended.
   465  }
   466  
   467  // Validate validates the basic filter options, setting sane defaults where appropriate.
   468  func (f *BasicFilter) Validate(nowFunc func() time.Time) error {
   469  	now := nowFunc().UTC().Truncate(time.Second)
   470  	if f.EndTime == nil || f.EndTime.IsZero() {
   471  		f.EndTime = &now
   472  	}
   473  
   474  	if f.StartTime == nil {
   475  		f.StartTime = &time.Time{}
   476  	}
   477  
   478  	if f.EndTime.Before(*(f.StartTime)) {
   479  		return errReversedTimes
   480  	}
   481  
   482  	return nil
   483  }