Skip to content

Commit

Permalink
Data: Deprecate TimeSeriesMany, Add Numeric Kind constants, link to D…
Browse files Browse the repository at this point in the history
…P docs (#560)

* add Kind(), IsNumeric(), TimeSeriesMulti, and update IsTimeSeries()
  • Loading branch information
kylebrandt committed Dec 2, 2022
1 parent 9f806a2 commit 2edccc9
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 81 deletions.
174 changes: 118 additions & 56 deletions data/frame_type.go
Expand Up @@ -6,68 +6,82 @@ 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 {
switch p {
case
FrameTypeTimeSeriesWide,
FrameTypeTimeSeriesLong,
FrameTypeTimeSeriesMany:
FrameTypeTimeSeriesMulti,
FrameTypeTimeSeriesMany,

FrameTypeNumericWide,
FrameTypeNumericLong,
FrameTypeNumericMulti:
return true
}
return false
Expand All @@ -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"
33 changes: 16 additions & 17 deletions experimental/sdata/timeseries/multi.go
Expand Up @@ -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()
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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")
}
Expand Down
14 changes: 7 additions & 7 deletions experimental/sdata/timeseries/multi_test.go
Expand Up @@ -82,23 +82,23 @@ func TestMultiFrameSeriesValidate_WithFrames_InvalidCases(t *testing.T) {
{
name: "frame with only value field is not valid, missing time field",
mfs: &timeseries.MultiFrame{
addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMany),
addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMulti),
data.NewField("", nil, []float64{})),
},
errContains: "missing a []time.Time field",
},
{
name: "frame with only a time field and no value is not valid",
mfs: &timeseries.MultiFrame{
addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMany),
addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMulti),
data.NewField("", nil, []time.Time{})),
},
errContains: "must have at least one value field",
},
{
name: "fields must be of the same length",
mfs: &timeseries.MultiFrame{
addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMany),
addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMulti),
data.NewField("", nil, []float64{1, 2}),
data.NewField("", nil, []time.Time{time.UnixMilli(1)})),
},
Expand All @@ -107,7 +107,7 @@ func TestMultiFrameSeriesValidate_WithFrames_InvalidCases(t *testing.T) {
{
name: "frame with unsorted time is not valid",
mfs: &timeseries.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)})),
},
Expand All @@ -117,10 +117,10 @@ func TestMultiFrameSeriesValidate_WithFrames_InvalidCases(t *testing.T) {
{
name: "duplicate metrics as identified by name + labels are invalid",
mfs: &timeseries.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)})),
},
Expand Down Expand Up @@ -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{}),
Expand Down
2 changes: 1 addition & 1 deletion experimental/sdata/timeseries/series.go
Expand Up @@ -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:
Expand Down

0 comments on commit 2edccc9

Please sign in to comment.