2021-06-08 16:56:33 +00:00
package influxdb
import (
"context"
2021-06-15 22:36:11 +00:00
"database/sql/driver"
2021-06-08 16:56:33 +00:00
"encoding/json"
2021-06-15 22:36:11 +00:00
"fmt"
2021-06-08 16:56:33 +00:00
"regexp"
"strings"
"time"
"unicode/utf8"
"github.com/influxdata/influxdb/v2/kit/platform"
"github.com/influxdata/influxdb/v2/kit/platform/errors"
)
var (
errEmptySummary = & errors . Error {
Code : errors . EInvalid ,
Msg : "summary cannot be empty" ,
}
errSummaryTooLong = & errors . Error {
Code : errors . EInvalid ,
Msg : "summary must be less than 255 characters" ,
}
errStreamTagTooLong = & errors . Error {
Code : errors . EInvalid ,
Msg : "stream tag must be less than 255 characters" ,
}
errStreamNameTooLong = & errors . Error {
Code : errors . EInvalid ,
Msg : "stream name must be less than 255 characters" ,
}
errStreamDescTooLong = & errors . Error {
Code : errors . EInvalid ,
Msg : "stream description must be less than 1024 characters" ,
}
errStickerTooLong = & errors . Error {
Code : errors . EInvalid ,
Msg : "stickers must be less than 255 characters" ,
}
errMsgTooLong = & errors . Error {
Code : errors . EInvalid ,
Msg : "message must be less than 4096 characters" ,
}
errReversedTimes = & errors . Error {
Code : errors . EInvalid ,
Msg : "start time must come before end time" ,
}
errMissingStreamName = & errors . Error {
Code : errors . EInvalid ,
Msg : "stream name must be set" ,
}
errMissingStreamTagOrId = & errors . Error {
Code : errors . EInvalid ,
Msg : "stream tag or id must be set" ,
}
errMissingEndTime = & errors . Error {
Code : errors . EInvalid ,
Msg : "end time must be set" ,
}
errMissingStartTime = & errors . Error {
Code : errors . EInvalid ,
Msg : "start time must be set" ,
}
)
2021-06-15 22:36:11 +00:00
func invalidStickerError ( s string ) error {
return & errors . Error {
Code : errors . EInternal ,
Msg : fmt . Sprintf ( "invalid sticker: %q" , s ) ,
}
}
func stickerSliceToMap ( stickers [ ] string ) ( map [ string ] string , error ) {
stickerMap := map [ string ] string { }
for i := range stickers {
2022-04-13 20:24:27 +00:00
if stick0 , stick1 , found := strings . Cut ( stickers [ i ] , "=" ) ; found {
stickerMap [ stick0 ] = stick1
} else {
2021-06-15 22:36:11 +00:00
return nil , invalidStickerError ( stickers [ i ] )
}
}
return stickerMap , nil
}
2022-04-13 20:24:27 +00:00
// AnnotationService is the service contract for Annotations
2021-06-08 16:56:33 +00:00
type AnnotationService interface {
// CreateAnnotations creates annotations.
CreateAnnotations ( ctx context . Context , orgID platform . ID , create [ ] AnnotationCreate ) ( [ ] AnnotationEvent , error )
// ListAnnotations lists all annotations matching the filter.
2021-06-11 21:39:51 +00:00
ListAnnotations ( ctx context . Context , orgID platform . ID , filter AnnotationListFilter ) ( [ ] StoredAnnotation , error )
2021-06-08 16:56:33 +00:00
// GetAnnotation gets an annotation by id.
2021-06-11 21:39:51 +00:00
GetAnnotation ( ctx context . Context , id platform . ID ) ( * StoredAnnotation , error )
2021-06-08 16:56:33 +00:00
// DeleteAnnotations deletes annotations matching the filter.
DeleteAnnotations ( ctx context . Context , orgID platform . ID , delete AnnotationDeleteFilter ) error
// DeleteAnnotation deletes an annotation by id.
2021-06-09 20:20:22 +00:00
DeleteAnnotation ( ctx context . Context , id platform . ID ) error
2021-06-08 16:56:33 +00:00
// UpdateAnnotation updates an annotation.
2021-06-09 20:20:22 +00:00
UpdateAnnotation ( ctx context . Context , id platform . ID , update AnnotationCreate ) ( * AnnotationEvent , error )
2021-06-08 16:56:33 +00:00
// ListStreams lists all streams matching the filter.
2021-06-11 21:39:51 +00:00
ListStreams ( ctx context . Context , orgID platform . ID , filter StreamListFilter ) ( [ ] StoredStream , error )
2021-06-08 16:56:33 +00:00
// CreateOrUpdateStream creates or updates the matching stream by name.
2021-06-11 21:39:51 +00:00
CreateOrUpdateStream ( ctx context . Context , orgID platform . ID , stream Stream ) ( * ReadStream , error )
// GetStream gets a stream by id. Currently this is only used for authorization, and there are no
// API routes for getting a single stream by ID.
GetStream ( ctx context . Context , id platform . ID ) ( * StoredStream , error )
2021-06-08 16:56:33 +00:00
// UpdateStream updates the stream by the ID.
2021-06-09 20:20:22 +00:00
UpdateStream ( ctx context . Context , id platform . ID , stream Stream ) ( * ReadStream , error )
// DeleteStreams deletes one or more streams by name.
2021-06-11 21:39:51 +00:00
DeleteStreams ( ctx context . Context , orgID platform . ID , delete BasicStream ) error
2021-06-08 16:56:33 +00:00
// DeleteStreamByID deletes the stream metadata by id.
2021-06-09 20:20:22 +00:00
DeleteStreamByID ( ctx context . Context , id platform . ID ) error
2021-06-08 16:56:33 +00:00
}
// AnnotationEvent contains fields for annotating an event.
type AnnotationEvent struct {
ID platform . ID ` json:"id,omitempty" ` // ID is the annotation ID.
AnnotationCreate // AnnotationCreate defines the common input/output bits of an annotation.
}
// AnnotationCreate contains user providable fields for annotating an event.
type AnnotationCreate struct {
2021-06-15 22:36:11 +00:00
StreamTag string ` json:"stream,omitempty" ` // StreamTag provides a means to logically group a set of annotated events.
Summary string ` json:"summary" ` // Summary is the only field required to annotate an event.
Message string ` json:"message,omitempty" ` // Message provides more details about the event being annotated.
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.
EndTime * time . Time ` json:"endTime,omitempty" ` // EndTime is the time of the event being annotated. Defaults to now if not set.
StartTime * time . Time ` json:"startTime,omitempty" ` // StartTime is the start time of the event being annotated. Defaults to EndTime if not set.
2021-06-08 16:56:33 +00:00
}
// StoredAnnotation represents annotation data to be stored in the database.
type StoredAnnotation struct {
2021-06-15 22:36:11 +00:00
ID platform . ID ` db:"id" ` // ID is the annotation's id.
OrgID platform . ID ` db:"org_id" ` // OrgID is the annotations's owning organization.
StreamID platform . ID ` db:"stream_id" ` // StreamID is the id of a stream.
StreamTag string ` db:"stream" ` // StreamTag is the name of a stream (when selecting with join of streams).
Summary string ` db:"summary" ` // Summary is the summary of the annotated event.
Message string ` db:"message" ` // Message is a longer description of the annotated event.
Stickers AnnotationStickers ` db:"stickers" ` // Stickers are additional labels to group annotations by.
Duration string ` db:"duration" ` // Duration is the time range (with zone) of an annotated event.
Lower string ` db:"lower" ` // Lower is the time an annotated event begins.
Upper string ` db:"upper" ` // Upper is the time an annotated event ends.
}
// ToCreate is a utility method for converting a StoredAnnotation to an AnnotationCreate type
func ( s StoredAnnotation ) ToCreate ( ) ( * AnnotationCreate , error ) {
et , err := time . Parse ( time . RFC3339Nano , s . Upper )
if err != nil {
return nil , err
}
st , err := time . Parse ( time . RFC3339Nano , s . Lower )
if err != nil {
return nil , err
}
return & AnnotationCreate {
StreamTag : s . StreamTag ,
Summary : s . Summary ,
Message : s . Message ,
Stickers : s . Stickers ,
EndTime : & et ,
StartTime : & st ,
} , nil
}
// ToEvent is a utility method for converting a StoredAnnotation to an AnnotationEvent type
func ( s StoredAnnotation ) ToEvent ( ) ( * AnnotationEvent , error ) {
c , err := s . ToCreate ( )
if err != nil {
return nil , err
}
return & AnnotationEvent {
ID : s . ID ,
AnnotationCreate : * c ,
} , nil
}
type AnnotationStickers map [ string ] string
// Value implements the database/sql Valuer interface for adding AnnotationStickers to the database
// Stickers are stored in the database as a slice of strings like "[key=val]"
// They are encoded into a JSON string for storing into the database, and the JSON sqlite extension is
// able to manipulate them like an object.
func ( a AnnotationStickers ) Value ( ) ( driver . Value , error ) {
stickSlice := make ( [ ] string , 0 , len ( a ) )
for k , v := range a {
stickSlice = append ( stickSlice , fmt . Sprintf ( "%s=%s" , k , v ) )
}
sticks , err := json . Marshal ( stickSlice )
if err != nil {
return nil , err
}
return string ( sticks ) , nil
}
// Scan implements the database/sql Scanner interface for retrieving AnnotationStickers from the database
// The string is decoded into a slice of strings, which are then converted back into a map
func ( a * AnnotationStickers ) Scan ( value interface { } ) error {
vString , ok := value . ( string )
if ! ok {
return & errors . Error {
Code : errors . EInternal ,
Msg : "could not load stickers from sqlite" ,
}
}
var stickSlice [ ] string
if err := json . NewDecoder ( strings . NewReader ( vString ) ) . Decode ( & stickSlice ) ; err != nil {
return err
}
stickMap , err := stickerSliceToMap ( stickSlice )
if err != nil {
return nil
}
* a = stickMap
return nil
2021-06-08 16:56:33 +00:00
}
// Validate validates the creation object.
func ( a * AnnotationCreate ) Validate ( nowFunc func ( ) time . Time ) error {
switch s := utf8 . RuneCountInString ( a . Summary ) ; {
case s <= 0 :
return errEmptySummary
case s > 255 :
return errSummaryTooLong
}
switch t := utf8 . RuneCountInString ( a . StreamTag ) ; {
case t == 0 :
a . StreamTag = "default"
case t > 255 :
return errStreamTagTooLong
}
if utf8 . RuneCountInString ( a . Message ) > 4096 {
return errMsgTooLong
}
for k , v := range a . Stickers {
if utf8 . RuneCountInString ( k ) > 255 || utf8 . RuneCountInString ( v ) > 255 {
return errStickerTooLong
}
}
now := nowFunc ( )
if a . EndTime == nil {
a . EndTime = & now
}
if a . StartTime == nil {
a . StartTime = a . EndTime
}
if a . EndTime . Before ( * ( a . StartTime ) ) {
return errReversedTimes
}
return nil
}
// AnnotationDeleteFilter contains fields for deleting an annotated event.
type AnnotationDeleteFilter struct {
StreamTag string ` json:"stream,omitempty" ` // StreamTag provides a means to logically group a set of annotated events.
StreamID platform . ID ` json:"streamID,omitempty" ` // StreamID provides a means to logically group a set of annotated events.
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.
EndTime * time . Time ` json:"endTime,omitempty" ` // EndTime is the time of the event being annotated. Defaults to now if not set.
StartTime * time . Time ` json:"startTime,omitempty" ` // StartTime is the start time of the event being annotated. Defaults to EndTime if not set.
}
// Validate validates the deletion object.
func ( a * AnnotationDeleteFilter ) Validate ( ) error {
var errs [ ] string
if len ( a . StreamTag ) == 0 && ! a . StreamID . Valid ( ) {
errs = append ( errs , errMissingStreamTagOrId . Error ( ) )
}
if a . EndTime == nil {
errs = append ( errs , errMissingEndTime . Error ( ) )
}
if a . StartTime == nil {
errs = append ( errs , errMissingStartTime . Error ( ) )
}
if len ( errs ) > 0 {
return & errors . Error {
Code : errors . EInvalid ,
Msg : strings . Join ( errs , "; " ) ,
}
}
if a . EndTime . Before ( * ( a . StartTime ) ) {
return errReversedTimes
}
return nil
}
var dre = regexp . MustCompile ( ` stickers\[(.*)\] ` )
// SetStickers sets the stickers from the query parameters.
func ( a * AnnotationDeleteFilter ) SetStickers ( vals map [ string ] [ ] string ) {
if a . Stickers == nil {
a . Stickers = map [ string ] string { }
}
for k , v := range vals {
if ss := dre . FindStringSubmatch ( k ) ; len ( ss ) == 2 && len ( v ) > 0 {
a . Stickers [ ss [ 1 ] ] = v [ 0 ]
}
}
}
// AnnotationList defines the structure of the response when listing annotations.
type AnnotationList struct {
StreamTag string ` json:"stream" `
Annotations [ ] ReadAnnotation ` json:"annotations" `
}
// ReadAnnotations allows annotations to be assigned to a stream.
type ReadAnnotations map [ string ] [ ] ReadAnnotation
// MarshalJSON allows us to marshal the annotations belonging to a stream properly.
func ( s ReadAnnotations ) MarshalJSON ( ) ( [ ] byte , error ) {
annotationList := [ ] AnnotationList { }
for k , v := range s {
annotationList = append ( annotationList , AnnotationList {
StreamTag : k ,
Annotations : v ,
} )
}
return json . Marshal ( annotationList )
}
// ReadAnnotation defines the simplest form of an annotation to be returned. Essentially, it's AnnotationEvent without stream info.
type ReadAnnotation struct {
ID platform . ID ` json:"id" ` // ID is the annotation's generated id.
Summary string ` json:"summary" ` // Summary is the only field required to annotate an event.
Message string ` json:"message,omitempty" ` // Message provides more details about the event being annotated.
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.
EndTime string ` json:"endTime" ` // EndTime is the time of the event being annotated.
StartTime string ` json:"startTime,omitempty" ` // StartTime is the start time of the event being annotated.
}
// AnnotationListFilter is a selection filter for listing annotations.
type AnnotationListFilter struct {
2021-06-15 22:36:11 +00:00
StickerIncludes AnnotationStickers ` json:"stickerIncludes,omitempty" ` // StickerIncludes allows the user to filter annotated events based on it's sticker.
StreamIncludes [ ] string ` json:"streamIncludes,omitempty" ` // StreamIncludes allows the user to filter annotated events by stream.
2021-06-08 16:56:33 +00:00
BasicFilter
}
// Validate validates the filter.
func ( f * AnnotationListFilter ) Validate ( nowFunc func ( ) time . Time ) error {
return f . BasicFilter . Validate ( nowFunc )
}
var re = regexp . MustCompile ( ` stickerIncludes\[(.*)\] ` )
// SetStickerIncludes sets the stickerIncludes from the query parameters.
func ( f * AnnotationListFilter ) SetStickerIncludes ( vals map [ string ] [ ] string ) {
if f . StickerIncludes == nil {
f . StickerIncludes = map [ string ] string { }
}
for k , v := range vals {
if ss := re . FindStringSubmatch ( k ) ; len ( ss ) == 2 && len ( v ) > 0 {
f . StickerIncludes [ ss [ 1 ] ] = v [ 0 ]
}
}
}
// StreamListFilter is a selection filter for listing streams. Streams are not considered first class resources, but depend on an annotation using them.
type StreamListFilter struct {
StreamIncludes [ ] string ` json:"streamIncludes,omitempty" ` // StreamIncludes allows the user to filter streams returned.
BasicFilter
}
// Validate validates the filter.
func ( f * StreamListFilter ) Validate ( nowFunc func ( ) time . Time ) error {
return f . BasicFilter . Validate ( nowFunc )
}
// Stream defines the stream metadata. Used in create and update requests/responses. Delete requests will only require stream name.
type Stream struct {
Name string ` json:"stream" ` // Name is the name of a stream.
Description string ` json:"description,omitempty" ` // Description is more information about a stream.
}
// ReadStream defines the returned stream.
type ReadStream struct {
ID platform . ID ` json:"id" db:"id" ` // ID is the id of a stream.
Name string ` json:"stream" db:"name" ` // Name is the name of a stream.
Description string ` json:"description,omitempty" db:"description" ` // Description is more information about a stream.
CreatedAt time . Time ` json:"createdAt" db:"created_at" ` // CreatedAt is a timestamp.
UpdatedAt time . Time ` json:"updatedAt" db:"updated_at" ` // UpdatedAt is a timestamp.
}
// IsValid validates the stream.
func ( s * Stream ) Validate ( strict bool ) error {
switch nameChars := utf8 . RuneCountInString ( s . Name ) ; {
case nameChars <= 0 :
if strict {
return errMissingStreamName
}
s . Name = "default"
case nameChars > 255 :
return errStreamNameTooLong
}
if utf8 . RuneCountInString ( s . Description ) > 1024 {
return errStreamDescTooLong
}
return nil
}
// StoredStream represents stream data to be stored in the metadata database.
type StoredStream struct {
ID platform . ID ` db:"id" ` // ID is the stream's id.
OrgID platform . ID ` db:"org_id" ` // OrgID is the stream's owning organization.
Name string ` db:"name" ` // Name is the name of a stream.
Description string ` db:"description" ` // Description is more information about a stream.
CreatedAt time . Time ` db:"created_at" ` // CreatedAt is a timestamp.
UpdatedAt time . Time ` db:"updated_at" ` // UpdatedAt is a timestamp.
}
// BasicStream defines a stream by name. Used for stream deletes.
type BasicStream struct {
Names [ ] string ` json:"stream" `
}
// IsValid validates the stream is not empty.
func ( s BasicStream ) IsValid ( ) bool {
if len ( s . Names ) <= 0 {
return false
}
for i := range s . Names {
if len ( s . Names [ i ] ) <= 0 {
return false
}
}
return true
}
// BasicFilter defines common filter options.
type BasicFilter struct {
StartTime * time . Time ` json:"startTime,omitempty" ` // StartTime is the time the event being annotated started.
EndTime * time . Time ` json:"endTime,omitempty" ` // EndTime is the time the event being annotated ended.
}
// Validate validates the basic filter options, setting sane defaults where appropriate.
func ( f * BasicFilter ) Validate ( nowFunc func ( ) time . Time ) error {
now := nowFunc ( ) . UTC ( ) . Truncate ( time . Second )
if f . EndTime == nil || f . EndTime . IsZero ( ) {
f . EndTime = & now
}
if f . StartTime == nil {
f . StartTime = & time . Time { }
}
if f . EndTime . Before ( * ( f . StartTime ) ) {
return errReversedTimes
}
return nil
}