diff --git a/data/frame_type.go b/data/frame_type.go index 00e803396..4fdafbeba 100644 --- a/data/frame_type.go +++ b/data/frame_type.go @@ -6,60 +6,69 @@ package data // the Frame correspond to a defined FrameType. type FrameType string -const ( - // FrameTypeUnknown indicates that we do not know the field type - FrameTypeUnknown FrameType = "" - - // FrameTypeTimeSeriesWide has at least two fields: - // field[0]: - // * type time - // * unique ascending values - // field[1..n]: - // * distinct labels may be attached to each field - // * numeric & boolean fields can be drawn as lines on a graph - // See https://grafana.com/docs/grafana/latest/developers/plugins/data-frames/#wide-format - FrameTypeTimeSeriesWide = "timeseries-wide" - - // FrameTypeTimeSeriesLong uses string fields to define dimensions. I has at least two fields: - // field[0]: - // * type time - // * ascending values - // * duplicate times exist for multiple dimensions - // field[1..n]: - // * string fields define series dimensions - // * non-string fields define the series progression - // See https://grafana.com/docs/grafana/latest/developers/plugins/data-frames/#long-format - FrameTypeTimeSeriesLong = "timeseries-long" - - // FrameTypeTimeSeriesMany is the same as "Wide" with exactly one numeric value field - // field[0]: - // * type time - // * ascending values - // field[1]: - // * number field - // * labels represent the series dimensions - // This structure is typically part of a list of frames with the same structure - FrameTypeTimeSeriesMany = "timeseries-many" - - // Soon? - // "timeseries-wide-ohlc" -- known fields for open/high/low/close - // "histogram" -- BucketMin, BucketMax, values... - // "trace" -- ?? - // "node-graph-nodes" - // "node-graph-edges" - - // FrameTypeDirectoryListing represents the items in a directory - // field[0]: - // * name - // * new paths can be constructed from the parent path + separator + name - // field[1]: - // * media-type - // * when "directory" it can be nested - FrameTypeDirectoryListing = "directory-listing" - - // FrameTypeTable represents an arbitrary table structure with no constraints - FrameTypeTable = "table" -) +// --- +// Docs Note: Constants need to be on their own line for links to work with the pkgsite docs. +// --- + +// FrameTypeUnknown indicates that we do not know the frame type +const FrameTypeUnknown FrameType = "" + +// FrameTypeTimeSeriesWide uses labels on fields to define dimensions and is documented in [Time Series Wide Format in the Data Plane Contract]. There is additional documentation in the [Developer Data Frame Documentation on the Wide Format]. +// +// [Time Series Wide Format in the Data Plane Contract]: https://github.com/grafana/grafana-plugin-sdk-go/blob/main/data/contract_docs/timeseries.md#time-series-wide-format-timeserieswide +// [Developer Data Frame Documentation on the Wide Format]: https://grafana.com/docs/grafana/latest/developers/plugins/data-frames/#wide-format +const FrameTypeTimeSeriesWide FrameType = "timeseries-wide" + +// FrameTypeTimeSeriesLong uses string fields to define dimensions and is documented in [Time Series Long Format in the Data Plane Contract]. There is additional documentation in the [Developer Data Frame Documentation on Long Format]. +// +// [Time Series Long Format in the Data Plane Contract]: https://github.com/grafana/grafana-plugin-sdk-go/blob/main/data/contract_docs/timeseries.md#time-series-long-format-timeserieslong-sql-like +// [Developer Data Frame Documentation on Long Format]: https://grafana.com/docs/grafana/latest/developers/plugins/data-frames/#long-format +const FrameTypeTimeSeriesLong FrameType = "timeseries-long" + +// FrameTypeTimeSeriesMany is the same as "Wide" with exactly one numeric value field. +// +// Deprecated: use FrameTypeTimeSeriesMulti instead. +const FrameTypeTimeSeriesMany FrameType = "timeseries-many" + +// FrameTypeTimeSeriesMulti is documented in the [Time Series Multi Format in the Data Plane Contract]. +// This replaces FrameTypeTimeSeriesMany. +// +// [Time Series Multi Format in the Data Plane Contract]: https://github.com/grafana/grafana-plugin-sdk-go/blob/main/data/contract_docs/timeseries.md#time-series-multi-format-timeseriesmulti +const FrameTypeTimeSeriesMulti FrameType = "timeseries-multi" + +// FrameTypeDirectoryListing represents the items in a directory +// field[0]: +// * name +// * new paths can be constructed from the parent path + separator + name +// field[1]: +// * media-type +// * when "directory" it can be nested +const FrameTypeDirectoryListing FrameType = "directory-listing" + +// FrameTypeTable represents an arbitrary table structure with no constraints. +const FrameTypeTable FrameType = "table" + +// FrameTypeNumericWide is documented in the [Numeric Wide Format in the Data Plane Contract]. +// +// [Numeric Wide Format in the Data Plane Contract]: https://github.com/grafana/grafana-plugin-sdk-go/blob/main/data/contract_docs/numeric.md#numeric-wide-format-numericwide +const FrameTypeNumericWide FrameType = "numeric-wide" + +// FrameTypeNumericMulti is documented in the [Numeric Multi Format in the Data Plane Contract]. +// +// [Numeric Multi Format in the Data Plane Contract]: https://github.com/grafana/grafana-plugin-sdk-go/blob/main/data/contract_docs/numeric.md#numeric-multi-format-numericmulti +const FrameTypeNumericMulti FrameType = "numeric-multi" + +// FrameTypeNumericLong is documented in the [Numeric Long Format in the Data Plane Contract]. +// +// [Numeric Long Format in the Data Plane Contract]: https://github.com/grafana/grafana-plugin-sdk-go/blob/main/data/contract_docs/numeric.md#numeric-long-format-numericlong-sql-table-like +const FrameTypeNumericLong FrameType = "numeric-long" + +// Soon? +// "timeseries-wide-ohlc" -- known fields for open/high/low/close +// "histogram" -- BucketMin, BucketMax, values... +// "trace" -- ?? +// "node-graph-nodes" +// "node-graph-edges" // IsKnownType checks if the value is a known structure func (p FrameType) IsKnownType() bool { @@ -67,7 +76,12 @@ func (p FrameType) IsKnownType() bool { case FrameTypeTimeSeriesWide, FrameTypeTimeSeriesLong, - FrameTypeTimeSeriesMany: + FrameTypeTimeSeriesMulti, + FrameTypeTimeSeriesMany, + + FrameTypeNumericWide, + FrameTypeNumericLong, + FrameTypeNumericMulti: return true } return false @@ -78,18 +92,66 @@ func FrameTypes() []FrameType { return []FrameType{ FrameTypeTimeSeriesWide, FrameTypeTimeSeriesLong, + FrameTypeTimeSeriesMulti, FrameTypeTimeSeriesMany, + + FrameTypeNumericWide, + FrameTypeNumericLong, + FrameTypeNumericMulti, } } -// IsTimeSeries checks if the type represents a timeseries +// IsTimeSeries checks if the FrameType is KindTimeSeries func (p FrameType) IsTimeSeries() bool { switch p { case FrameTypeTimeSeriesWide, FrameTypeTimeSeriesLong, + FrameTypeTimeSeriesMulti, FrameTypeTimeSeriesMany: return true } return false } + +// IsNumeric checks if the FrameType is KindNumeric. +func (p FrameType) IsNumeric() bool { + switch p { + case + FrameTypeNumericWide, + FrameTypeNumericLong, + FrameTypeNumericMulti: + return true + } + return false +} + +// Kind returns the FrameTypeKind from the FrameType. +func (p FrameType) Kind() FrameTypeKind { + switch { + case p.IsTimeSeries(): + return KindTimeSeries + case p.IsNumeric(): + return KindNumeric + default: + return KindUnknown + } +} + +// FrameTypeKind represents the Kind a particular FrameType falls into. See [Kinds and Formats] in +// the data plane documentation. +// +// [Kinds and Formats]: https://github.com/grafana/grafana-plugin-sdk-go/tree/main/data/contract_docs#kinds-and-formats +type FrameTypeKind string + +const KindUnknown FrameTypeKind = "" + +// KindTimeSeries means the FrameType's Kind is time series. See [Data Plane Time Series Kind]. +// +// [Data Plane Time Series Kind]: https://github.com/grafana/grafana-plugin-sdk-go/blob/main/data/contract_docs/timeseries.md +const KindTimeSeries FrameTypeKind = "timeseries" + +// KindNumeric means the FrameType's Kind is numeric. See [Data Plane Numeric Kind]. +// +// [Data Plane Numeric Kind]: https://github.com/grafana/grafana-plugin-sdk-go/blob/main/data/contract_docs/numeric.md +const KindNumeric FrameTypeKind = "numeric" diff --git a/experimental/sdata/timeseries/multi.go b/experimental/sdata/timeseries/multi.go index 465dfe79a..1621abf00 100644 --- a/experimental/sdata/timeseries/multi.go +++ b/experimental/sdata/timeseries/multi.go @@ -19,7 +19,7 @@ type MultiFrame []*data.Frame // The returned MultiFrame is a valid typed data response that corresponds to "No Data". func NewMultiFrame() *MultiFrame { return &MultiFrame{ - emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMany), + emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMulti), } // Consider: MultiFrame.New() } @@ -55,7 +55,7 @@ func (mfs *MultiFrame) AddSeries(metricName string, l data.Labels, t []time.Time if len(*mfs) == 1 && len((*mfs)[0].Fields) == 0 { // update empty response placeholder frame (*mfs)[0].Fields = append((*mfs)[0].Fields, timeField, valueField) } else { - frame := emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMany) + frame := emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMulti) frame.Fields = append(frame.Fields, timeField, valueField) *mfs = append(*mfs, frame) } @@ -74,21 +74,20 @@ func (mfs *MultiFrame) GetMetricRefs(validateData bool) ([]MetricRef, []sdata.Fr Generally, when the type indicator in present on a frame, we become stricter on what the shape of the frame can be. However, there are still degrees of freedom: - extra frames without the indicator, or extra fields when the indicator is present. - Rules -- Whenever an error is returned, there are no ignored fields returned -- Must have at least one frame -- The first frame may have no fields, if so it is considered the empty response case -- The first frame must be valid or will error, additional invalid frames with the type indicator will error, + - Whenever an error is returned, there are no ignored fields returned + - Must have at least one frame + - The first frame may have no fields, if so it is considered the empty response case + - The first frame must be valid or will error, additional invalid frames with the type indicator will error, frames without type indicator are ignored -- A valid individual Frame (in the non empty case) has: - - The type indicator - - a []time.Time field (not []*time.Time) sorted from oldest to newest - - a numeric value field -- Any nil Frames or Fields will cause an error (e.g. [Frame, Frame, nil, Frame] or [nil]) -- If any frame has fields within the frame of different lengths, an error will be returned -- If validateData is true, duplicate labels and sorted time fields will error, otherwise only the schema/metadata is checked. -- If all frames and their fields are ignored, and it is not the empty response case, an error is returned + - A valid individual Frame (in the non empty case) has: + - The type indicator + - a []time.Time field (not []*time.Time) sorted from oldest to newest + - a numeric value field + - Any nil Frames or Fields will cause an error (e.g. [Frame, Frame, nil, Frame] or [nil]) + - If any frame has fields within the frame of different lengths, an error will be returned + - If validateData is true, duplicate labels and sorted time fields will error, otherwise only the schema/metadata is checked. + - If all frames and their fields are ignored, and it is not the empty response case, an error is returned When things get ignored - Frames that don't have the type indicator as long as they are not first @@ -107,7 +106,7 @@ func validateAndGetRefsMulti(mfs *MultiFrame, validateData bool) (refs []MetricR return nil, nil, fmt.Errorf("frame 0 is nil which is invalid") case firstFrame.Meta == nil: return nil, nil, fmt.Errorf("frame 0 is missing a type indicator") - case !frameHasType(firstFrame, data.FrameTypeTimeSeriesMany): + case !frameHasType(firstFrame, data.FrameTypeTimeSeriesMulti): return nil, nil, fmt.Errorf("frame 0 has wrong type, expected many/multi but got %q", firstFrame.Meta.Type) case len(firstFrame.Fields) == 0: if len(*mfs) > 1 { @@ -133,7 +132,7 @@ func validateAndGetRefsMulti(mfs *MultiFrame, validateData bool) (refs []MetricR } } - if !frameHasType(frame, data.FrameTypeTimeSeriesMany) { + if !frameHasType(frame, data.FrameTypeTimeSeriesMulti) { if frameIdx == 0 { return nil, nil, fmt.Errorf("first frame must have the many/multi type indicator in frame metadata") } diff --git a/experimental/sdata/timeseries/multi_test.go b/experimental/sdata/timeseries/multi_test.go index 00c44f853..e32610a16 100644 --- a/experimental/sdata/timeseries/multi_test.go +++ b/experimental/sdata/timeseries/multi_test.go @@ -82,7 +82,7 @@ func TestMultiFrameSeriesValidate_WithFrames_InvalidCases(t *testing.T) { { name: "frame with only value field is not valid, missing time field", mfs: ×eries.MultiFrame{ - addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMany), + addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMulti), data.NewField("", nil, []float64{})), }, errContains: "missing a []time.Time field", @@ -90,7 +90,7 @@ func TestMultiFrameSeriesValidate_WithFrames_InvalidCases(t *testing.T) { { name: "frame with only a time field and no value is not valid", mfs: ×eries.MultiFrame{ - addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMany), + addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMulti), data.NewField("", nil, []time.Time{})), }, errContains: "must have at least one value field", @@ -98,7 +98,7 @@ func TestMultiFrameSeriesValidate_WithFrames_InvalidCases(t *testing.T) { { name: "fields must be of the same length", mfs: ×eries.MultiFrame{ - addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMany), + addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMulti), data.NewField("", nil, []float64{1, 2}), data.NewField("", nil, []time.Time{time.UnixMilli(1)})), }, @@ -107,7 +107,7 @@ func TestMultiFrameSeriesValidate_WithFrames_InvalidCases(t *testing.T) { { name: "frame with unsorted time is not valid", mfs: ×eries.MultiFrame{ - addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMany), + addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMulti), data.NewField("", nil, []float64{1, 2}), data.NewField("", nil, []time.Time{time.UnixMilli(2), time.UnixMilli(1)})), }, @@ -117,10 +117,10 @@ func TestMultiFrameSeriesValidate_WithFrames_InvalidCases(t *testing.T) { { name: "duplicate metrics as identified by name + labels are invalid", mfs: ×eries.MultiFrame{ - addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMany), + addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMulti), data.NewField("os.cpu", data.Labels{"host": "a", "iface": "eth0"}, []float64{1, 2}), data.NewField("", nil, []time.Time{time.UnixMilli(1), time.UnixMilli(2)})), - addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMany), + addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMulti), data.NewField("os.cpu", data.Labels{"iface": "eth0", "host": "a"}, []float64{1, 2}), data.NewField("", nil, []time.Time{time.UnixMilli(1), time.UnixMilli(2)})), }, @@ -167,7 +167,7 @@ func TestMultiFrameSeriesGetMetricRefs_Empty_Invalid_Edge_Cases(t *testing.T) { s := timeseries.NewMultiFrame() // (s.AddMetric) would alter the first frame which would be the "right thing" to do. - *s = append(*s, emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMany)) + *s = append(*s, emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMulti)) (*s)[1].Fields = append((*s)[1].Fields, data.NewField("time", nil, []time.Time{}), data.NewField("cpu", nil, []float64{}), diff --git a/experimental/sdata/timeseries/series.go b/experimental/sdata/timeseries/series.go index 0fa764a02..7e5f04022 100644 --- a/experimental/sdata/timeseries/series.go +++ b/experimental/sdata/timeseries/series.go @@ -44,7 +44,7 @@ func CollectionReaderFromFrames(frames []*data.Frame) (CollectionReader, error) var tcr CollectionReader switch { - case mt == data.FrameTypeTimeSeriesMany: // aka multi + case mt == data.FrameTypeTimeSeriesMulti: mfs := MultiFrame(frames) tcr = &mfs case mt == data.FrameTypeTimeSeriesLong: