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 }