├── docs ├── contract │ ├── README.md │ ├── images │ │ └── data-types.jpg │ ├── dataframes.md │ ├── logs.md │ ├── numeric.md │ ├── heatmap.md │ ├── contract.md │ └── contract-spec.md ├── README.md └── img │ └── logo.svg ├── docusaurus └── website │ ├── static │ ├── .nojekyll │ ├── img │ │ ├── favicon.png │ │ ├── hero_banner.png │ │ └── logo.svg │ └── font │ │ ├── inter │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2 │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7SUc.woff2 │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7SUc.woff2 │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7SUc.woff2 │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff2 │ │ ├── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7SUc.woff2 │ │ └── UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7SUc.woff2 │ │ └── roboto │ │ └── L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2 │ ├── .gitignore │ ├── babel.config.js │ ├── tsconfig.json │ ├── src │ ├── theme │ │ ├── DocSidebar │ │ │ ├── Desktop │ │ │ │ ├── Content │ │ │ │ │ ├── styles.module.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── styles.module.css │ │ │ │ ├── CollapseButton │ │ │ │ │ ├── styles.module.css │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ └── Mobile │ │ │ │ └── index.tsx │ │ ├── tracking │ │ │ ├── cookie.ts │ │ │ └── index.ts │ │ ├── prism.js │ │ └── Root.tsx │ ├── pages │ │ ├── index.module.css │ │ └── index.tsx │ ├── components │ │ └── CookieConsent │ │ │ ├── CookieConsent.tsx │ │ │ └── styles.module.css │ ├── utils │ │ ├── useOneTrustIntegration.js │ │ └── oneTrustLoader.js │ └── css │ │ └── theme.css │ ├── sidebars.js │ ├── package.json │ ├── docusaurus.config.js │ ├── docusaurus.config.devportal.prod.js │ └── docusaurus.config.devportal.js ├── .gitignore ├── go.mod ├── go.work ├── .markdownlint.json ├── sdata ├── package.json ├── README.md ├── timeseries │ ├── convert.go │ ├── long_test.go │ ├── convert_test.go │ ├── wide_test.go │ ├── util.go │ ├── series_test.go │ ├── series.go │ ├── long.go │ └── wide.go ├── go.mod ├── numeric │ ├── util.go │ ├── numeric.go │ ├── wide.go │ ├── long.go │ ├── multi.go │ └── numeric_test.go ├── common.go └── reader │ ├── play_test.go │ └── reader.go ├── examples ├── package.json ├── data │ ├── numeric │ │ ├── long │ │ │ └── v0.1 │ │ │ │ └── basic_valid │ │ │ │ ├── numeric-long_no-data.json │ │ │ │ ├── numeric-long_two-items-by-dimension.json │ │ │ │ ├── numeric-long_empty-two-item-names.json │ │ │ │ ├── numeric-long_four-items-by-name-and-dimension.json │ │ │ │ └── numeric-long_four-items-by-name-and-dimension-two-labels.json │ │ ├── wide │ │ │ └── v0.1 │ │ │ │ ├── basic_valid │ │ │ │ ├── numeric-wide_no-data.json │ │ │ │ ├── numeric-wide_two-empty-items.json │ │ │ │ ├── numeric-wide_two-items-by-dimension.json │ │ │ │ ├── numeric-wide_two-items-by-dimension-name.json │ │ │ │ └── numeric-wide_four-items-by-dimension-name.json │ │ │ │ └── extended_valid │ │ │ │ └── numeric-wide_two-items-by-dimension-with-remainder-time.json │ │ └── multi │ │ │ └── v0.1 │ │ │ ├── basic_valid │ │ │ ├── numeric-multi_no-data.json │ │ │ ├── numeric-multi_two-items-by-dimension.json │ │ │ ├── numeric-multi_two-empty-items.json │ │ │ ├── numeric-multi_two-items-by-dimension-name-dif-name-dim.json │ │ │ └── numeric-multi_four-items-by-dimension-name.json │ │ │ └── extended_valid │ │ │ └── numeric-multi_two-items-by-dimension-with-remainder-time.json │ └── timeseries │ │ ├── long │ │ └── v0.1 │ │ │ └── basic_valid │ │ │ ├── timeseries-long_no-data.json │ │ │ ├── timeseries-long_two-items-by-dimension.json │ │ │ ├── timeseries-long_empty-two-item-names.json │ │ │ └── timeseries-long_four-items-by-name-and-dimension.json │ │ ├── wide │ │ └── v0.1 │ │ │ ├── basic_valid │ │ │ ├── timeseries-wide_no-data.json │ │ │ ├── timeseries-wide_one-item-no-name-or-labels.json │ │ │ ├── timeseries-wide_empty-one-item.json │ │ │ ├── timeseries-wide_one-item-with-name-and-labels.json │ │ │ └── timeseries-wide_two-items-by-dimension.json │ │ │ └── extended_valid │ │ │ ├── timeseries-wide_one-item-with-remainder-string.json │ │ │ └── timeseries-wide_one-item-with-remainder-time.json │ │ └── multi │ │ └── v0.1 │ │ ├── basic_valid │ │ ├── timeseries-multi_no-data.json │ │ ├── timeseries-multi_empty-one-item.json │ │ └── timeseries-multi_two-items-by-dimension-unaligned-time.json │ │ └── extended_valid │ │ └── timeseries-multi_two-items-by-dimension-unaligned-time-with-remainder-string.json ├── README.md ├── go.mod ├── examples_test.go └── examples_sort.go ├── turbo.json ├── cspell.config.json ├── .github └── workflows │ ├── ci.yml │ ├── deploy-to-developer-portal-dev.yml │ └── deploy-to-developer-portal-prod.yml ├── README.md └── package.json /docs/contract/README.md: -------------------------------------------------------------------------------- 1 | contract.md -------------------------------------------------------------------------------- /docusaurus/website/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | temp/ 4 | .turbo/ -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/grafana/dataplane 2 | 3 | go 1.22.4 4 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.22.4 2 | 3 | use ( 4 | ./examples 5 | ./sdata 6 | ) 7 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Grafana - Data Plane 2 | 3 | - [Contract docs](./contract/) 4 | -------------------------------------------------------------------------------- /docusaurus/website/.gitignore: -------------------------------------------------------------------------------- 1 | .docusaurus/ 2 | node_modules/ 3 | build/ 4 | .turbo/ -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "first-line-h1": false, 3 | "line-length": false, 4 | "no-inline-html": false 5 | } 6 | -------------------------------------------------------------------------------- /docs/contract/images/data-types.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/dataplane/HEAD/docs/contract/images/data-types.jpg -------------------------------------------------------------------------------- /docusaurus/website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docusaurus/website/static/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/dataplane/HEAD/docusaurus/website/static/img/favicon.png -------------------------------------------------------------------------------- /docusaurus/website/static/img/hero_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/dataplane/HEAD/docusaurus/website/static/img/hero_banner.png -------------------------------------------------------------------------------- /sdata/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@grafana/sdata", 3 | "private": true, 4 | "scripts": { 5 | "test:backend": "go test -v ./..." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@grafana/dataplane-examples", 3 | "private": true, 4 | "scripts": { 5 | "test:backend": "go test -v ./..." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /docusaurus/website/static/font/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/dataplane/HEAD/docusaurus/website/static/font/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7.woff2 -------------------------------------------------------------------------------- /docusaurus/website/static/font/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7SUc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/dataplane/HEAD/docusaurus/website/static/font/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7SUc.woff2 -------------------------------------------------------------------------------- /docusaurus/website/static/font/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7SUc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/dataplane/HEAD/docusaurus/website/static/font/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7SUc.woff2 -------------------------------------------------------------------------------- /docusaurus/website/static/font/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7SUc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/dataplane/HEAD/docusaurus/website/static/font/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7SUc.woff2 -------------------------------------------------------------------------------- /docusaurus/website/static/font/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/dataplane/HEAD/docusaurus/website/static/font/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff2 -------------------------------------------------------------------------------- /docusaurus/website/static/font/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7SUc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/dataplane/HEAD/docusaurus/website/static/font/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7SUc.woff2 -------------------------------------------------------------------------------- /docusaurus/website/static/font/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7SUc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/dataplane/HEAD/docusaurus/website/static/font/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7SUc.woff2 -------------------------------------------------------------------------------- /docusaurus/website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@tsconfig/docusaurus/tsconfig.json", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /docusaurus/website/static/font/roboto/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/dataplane/HEAD/docusaurus/website/static/font/roboto/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2 -------------------------------------------------------------------------------- /sdata/README.md: -------------------------------------------------------------------------------- 1 | # Grafana dataplane sdata 2 | 3 | > This `sdata` package is still in **experimental** stage. Things may break 4 | 5 | **sdata** package provides utilities to build data frames in a structural way and ensure the data frames adhere to data frame contract. 6 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "docs:build": { 5 | "outputs": [] 6 | }, 7 | "docs:start": { 8 | "outputs": [] 9 | }, 10 | "test": { 11 | "inputs": ["*.go", "go.mod"], 12 | "outputs": [] 13 | }, 14 | "test:backend": { 15 | "inputs": ["*.go", "go.mod"], 16 | "outputs": [] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docusaurus/website/src/theme/DocSidebar/Desktop/Content/styles.module.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 997px) { 2 | .menu { 3 | flex-grow: 1; 4 | padding: 0.5rem; 5 | } 6 | @supports (scrollbar-gutter: stable) { 7 | .menu { 8 | padding: 0.5rem 0 0.5rem 0.5rem; 9 | scrollbar-gutter: stable; 10 | } 11 | } 12 | 13 | .menuWithAnnouncementBar { 14 | margin-bottom: var(--docusaurus-announcement-bar-height); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/data/numeric/long/v0.1/basic_valid/numeric-long_no-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "numeric-long", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "NumericLong in no-data form (0 length fields).", 10 | "itemCount": 0, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": true 14 | } 15 | } 16 | }, 17 | "fields": [] 18 | }, 19 | "data": { 20 | "values": [] 21 | } 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /examples/data/numeric/wide/v0.1/basic_valid/numeric-wide_no-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "numeric-wide", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "NumericWide in no-data form (0 length fields).", 10 | "itemCount": 0, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": true 14 | } 15 | } 16 | }, 17 | "fields": [] 18 | }, 19 | "data": { 20 | "values": [] 21 | } 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /examples/data/numeric/multi/v0.1/basic_valid/numeric-multi_no-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "numeric-multi", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "NumericMulti in no-data form (0 length fields).", 10 | "itemCount": 0, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": true 14 | } 15 | } 16 | }, 17 | "fields": [] 18 | }, 19 | "data": { 20 | "values": [] 21 | } 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /examples/data/timeseries/long/v0.1/basic_valid/timeseries-long_no-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "timeseries-long", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "TimeseriesLong in no-data form (0 length fields).", 10 | "itemCount": 0, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": true 14 | } 15 | } 16 | }, 17 | "fields": [] 18 | }, 19 | "data": { 20 | "values": [] 21 | } 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /examples/data/timeseries/wide/v0.1/basic_valid/timeseries-wide_no-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "timeseries-wide", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "TimeseriesWide in no-data form (0 length fields).", 10 | "itemCount": 0, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": true 14 | } 15 | } 16 | }, 17 | "fields": [] 18 | }, 19 | "data": { 20 | "values": [] 21 | } 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /examples/data/timeseries/multi/v0.1/basic_valid/timeseries-multi_no-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "timeseries-multi", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "TimeseriesMulti in no-data form (0 length fields).", 10 | "itemCount": 0, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": true 14 | } 15 | } 16 | }, 17 | "fields": [] 18 | }, 19 | "data": { 20 | "values": [] 21 | } 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /cspell.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePaths": [ 3 | "node_modules/", 4 | "go.work", 5 | "**/*_test.go", 6 | "./docusaurus/website/**/*.{tsx,css}" 7 | ], 8 | "words": [ 9 | "dataframe", 10 | "dataframes", 11 | "dataplane", 12 | "datapoint", 13 | "datapoints", 14 | "datasource", 15 | "datasources", 16 | "datatypes", 17 | "devportal", 18 | "grafana", 19 | "Heatmaps", 20 | "jsoniter", 21 | "Milli", 22 | "scanline", 23 | "scanlines", 24 | "sdata", 25 | "testdata", 26 | "timeseries", 27 | "typecheck", 28 | "Visualisation" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /sdata/timeseries/convert.go: -------------------------------------------------------------------------------- 1 | package timeseries 2 | 3 | // LongToMulti converts a LongFrame into a MultiFrame and returns an error if it fails to parse the LongFrame. 4 | func LongToMulti(longFrame *LongFrame) (*MultiFrame, error) { 5 | collection, err := longFrame.GetCollection(false) 6 | if err != nil { 7 | return nil, err 8 | } 9 | 10 | multiFrame, err := NewMultiFrame(collection.RefID, MultiFrameVersionLatest) 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | for _, ref := range collection.Refs { 16 | multiFrame.addSeriesFields(ref.TimeField, ref.ValueField) 17 | } 18 | return multiFrame, nil 19 | } 20 | -------------------------------------------------------------------------------- /docusaurus/website/sidebars.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 3 | const sidebars = { 4 | doc: { 5 | Contract: [ 6 | { id: "contract", label: "Intro", type: "doc" }, 7 | { id: "dataframes", label: "Data frames", type: "doc" }, 8 | { id: "contract-spec", label: "Data plane contract spec", type: "doc" }, 9 | { id: "timeseries", label: "Timeseries", type: "doc" }, 10 | { id: "numeric", label: "Numeric", type: "doc" }, 11 | { id: "logs", label: "Logs", type: "doc" }, 12 | { id: "heatmap", label: "Heatmap", type: "doc" }, 13 | ], 14 | }, 15 | }; 16 | 17 | module.exports = sidebars; 18 | -------------------------------------------------------------------------------- /docusaurus/website/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | aspect-ratio: 3.01/1; 8 | background-image: url("/img/hero_banner.png"); 9 | background-size: contain; 10 | display: flex; 11 | align-items: center; 12 | padding: 1rem; 13 | text-align: left; 14 | position: relative; 15 | overflow: hidden; 16 | } 17 | 18 | .heroSubtitle { 19 | border-left: 4px solid; 20 | border-image: var(--grafana-gradient-brand-vertical) 1; 21 | } 22 | 23 | @media screen and (min-width: 768px) { 24 | .heroBannerWrapper { 25 | line-height: 1.2; 26 | margin-left: 3rem; 27 | max-width: 50%; 28 | } 29 | } 30 | 31 | @media screen and (min-width: 1366px) { 32 | .heroBannerWrapper { 33 | margin-left: 5rem; 34 | max-width: 36%; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docusaurus/website/src/components/CookieConsent/CookieConsent.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEventHandler } from 'react'; 2 | import styles from './styles.module.css'; 3 | 4 | type Props = { 5 | onClick?: MouseEventHandler; 6 | }; 7 | 8 | export function CookieConsent({ onClick }: Props) { 9 | return ( 10 |
11 |
12 |
13 | Grafana Labs uses cookies for the normal operation of this website.{' '} 14 | 15 | Learn more. 16 | 17 |
18 |
19 | 22 |
23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /docusaurus/website/src/theme/tracking/cookie.ts: -------------------------------------------------------------------------------- 1 | import cookie from 'cookiejs'; 2 | 3 | export const cookieName = 'consent'; 4 | export const analyticsVersion = '2'; 5 | 6 | export function getCookie(name: string, key: string) { 7 | let res = cookie.get(name); 8 | 9 | try { 10 | if (res && typeof res === 'string') { 11 | const parsed = JSON.parse(decodeURIComponent(res)); 12 | if (parsed[key] !== undefined) { 13 | return parsed[key]; 14 | } 15 | } 16 | } catch (e) { 17 | // do nothing 18 | } 19 | if (key === undefined) { 20 | return res; 21 | } 22 | } 23 | 24 | export function setCookie(name: string, value: any) { 25 | let val = value; 26 | 27 | if (typeof value === 'object') { 28 | val = JSON.stringify(value); 29 | } 30 | 31 | return cookie.set(name, val, { 32 | expires: 365, 33 | domain: `.${window.location.hostname}`, 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /docusaurus/website/src/theme/DocSidebar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useWindowSize} from '@docusaurus/theme-common'; 3 | import DocSidebarDesktop from '@theme/DocSidebar/Desktop'; 4 | import DocSidebarMobile from '@theme/DocSidebar/Mobile'; 5 | import type {Props} from '@theme/DocSidebar'; 6 | 7 | export default function DocSidebar(props: Props): JSX.Element { 8 | const windowSize = useWindowSize(); 9 | 10 | // Desktop sidebar visible on hydration: need SSR rendering 11 | const shouldRenderSidebarDesktop = 12 | windowSize === 'desktop' || windowSize === 'ssr'; 13 | 14 | // Mobile sidebar not visible on hydration: can avoid SSR rendering 15 | const shouldRenderSidebarMobile = windowSize === 'mobile'; 16 | 17 | return ( 18 | <> 19 | {shouldRenderSidebarDesktop && } 20 | {shouldRenderSidebarMobile && } 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | permissions: {} 3 | 4 | on: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')" 13 | name: Run unit tests 14 | permissions: 15 | contents: read #Clone repo 16 | statuses: write # Update GitHub status check with deploy preview link. 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v3 20 | with: 21 | persist-credentials: false 22 | ref: ${{ github.ref }} 23 | - name: Install dependencies 24 | run: yarn install --immutable --prefer-offline 25 | - name: Setup Go environment 26 | uses: actions/setup-go@v3 27 | with: 28 | go-version: "1.22.4" 29 | - name: Test Backend 30 | run: yarn test:backend 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Grafana Logo 8 |

Grafana Data Plane

9 |

Grafana data plane tools and docs

10 |
11 |
12 | Tests & builds status 17 |
18 |
19 |
20 | 21 | This is a monorepo of Grafana dataplane tools and docs 22 | 23 | ## Docs 24 | - [Data Plane Contract - Technical Specification](https://grafana.github.io/dataplane/contract/) 25 | 26 | ## backend packages 27 | 28 | - [sdata](./sdata/) (**experimental** Structural way of building typed dataframes) 29 | - [examples](./examples/) (Examples of dataplane typed dataframes in json files, and a go library for using them in tests) 30 | -------------------------------------------------------------------------------- /docusaurus/website/src/components/CookieConsent/styles.module.css: -------------------------------------------------------------------------------- 1 | .cookieConsent { 2 | bottom: 0; 3 | left: 0; 4 | position: fixed; 5 | right: 0; 6 | z-index: 1; 7 | border-top: 1px solid color-mix(in srgb, var(--ifm-color-warning) 20%, transparent); 8 | 9 | } 10 | 11 | .cookieConsentContainer { 12 | align-items: center; 13 | background-color: var(--ifm-color-black); 14 | display: flex; 15 | flex-direction: column; 16 | flex-wrap: wrap; 17 | justify-content: space-between; 18 | padding: 10px; 19 | width: 100%; 20 | } 21 | 22 | @media (min-width: 768px) { 23 | .cookieConsentContainer { 24 | flex-direction: row; 25 | padding-left: 40px; 26 | padding-right: 40px; 27 | } 28 | } 29 | 30 | .cookieConsentCta { 31 | background-color: var(--ifm-color-info); 32 | border: 1px solid var(--ifm-color-info); 33 | border-radius: 3px; 34 | cursor: pointer; 35 | font-size: 1rem; 36 | font-weight: bold; 37 | min-height: 2.5rem; 38 | padding: 0 1.5rem; 39 | } 40 | -------------------------------------------------------------------------------- /docusaurus/website/src/theme/DocSidebar/Desktop/styles.module.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 997px) { 2 | .sidebar { 3 | background-color: var(--ifm-navbar-background-color); 4 | display: flex; 5 | flex-direction: column; 6 | font-size: var(--size-md); 7 | height: 100%; 8 | padding-top: var(--ifm-navbar-height); 9 | width: var(--doc-sidebar-width); 10 | } 11 | 12 | .sidebarWithHideableNavbar { 13 | padding-top: 0; 14 | } 15 | 16 | .sidebarHidden { 17 | opacity: 0; 18 | visibility: hidden; 19 | } 20 | 21 | .sidebarLogo { 22 | display: flex !important; 23 | align-items: center; 24 | margin: 0 var(--ifm-navbar-padding-horizontal); 25 | min-height: var(--ifm-navbar-height); 26 | max-height: var(--ifm-navbar-height); 27 | color: inherit !important; 28 | text-decoration: none !important; 29 | } 30 | 31 | .sidebarLogo img { 32 | margin-right: 0.5rem; 33 | height: 2rem; 34 | } 35 | } 36 | 37 | .sidebarLogo { 38 | display: none; 39 | } 40 | -------------------------------------------------------------------------------- /examples/data/timeseries/wide/v0.1/basic_valid/timeseries-wide_one-item-no-name-or-labels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "timeseries-wide", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "TimeseriesWide with 1 items. There is no name or labels.", 10 | "itemCount": 1, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "name": "t", 20 | "type": "time", 21 | "typeInfo": { 22 | "frame": "time.Time" 23 | } 24 | }, 25 | { 26 | "type": "number", 27 | "typeInfo": { 28 | "frame": "float64" 29 | } 30 | } 31 | ] 32 | }, 33 | "data": { 34 | "values": [ 35 | [1664901845976, 1664902845976], 36 | [3, 5] 37 | ] 38 | } 39 | } 40 | ] 41 | -------------------------------------------------------------------------------- /examples/data/timeseries/multi/v0.1/basic_valid/timeseries-multi_empty-one-item.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "timeseries-multi", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "TimeseriesMulti with 1 empty items. There is 1 item name (slothCount) and 1 dimension (city).", 10 | "itemCount": 1, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "name": "t", 20 | "type": "time", 21 | "typeInfo": { 22 | "frame": "time.Time" 23 | } 24 | }, 25 | { 26 | "type": "number", 27 | "name": "slothCount", 28 | "typeInfo": { 29 | "frame": "float64" 30 | }, 31 | "labels": { 32 | "city": "LGA" 33 | } 34 | } 35 | ] 36 | }, 37 | "data": { 38 | "values": [] 39 | } 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /examples/data/timeseries/wide/v0.1/basic_valid/timeseries-wide_empty-one-item.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "timeseries-wide", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "TimeseriesWide with 1 empty items. There is 1 item name (slothCount) and 1 dimension (city).", 10 | "itemCount": 1, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "name": "t", 20 | "type": "time", 21 | "typeInfo": { 22 | "frame": "time.Time" 23 | } 24 | }, 25 | { 26 | "name": "slothCount", 27 | "type": "number", 28 | "typeInfo": { 29 | "frame": "float64" 30 | }, 31 | "labels": { 32 | "city": "LGA" 33 | } 34 | } 35 | ] 36 | }, 37 | "data": { 38 | "values": [[], []] 39 | } 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /examples/data/numeric/long/v0.1/basic_valid/numeric-long_two-items-by-dimension.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "numeric-long", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "NumericLong with 2 items. 1 name (avgSlothCount) and 1 dimension (city).", 10 | "itemCount": 2, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "type": "number", 20 | "name": "avgSlothCount", 21 | "typeInfo": { 22 | "frame": "float64" 23 | }, 24 | "labels": {} 25 | }, 26 | { 27 | "type": "string", 28 | "name": "city", 29 | "typeInfo": { 30 | "frame": "string" 31 | }, 32 | "labels": {} 33 | } 34 | ] 35 | }, 36 | "data": { 37 | "values": [ 38 | [4, 7.5], 39 | ["LGA", "MIA"] 40 | ] 41 | } 42 | } 43 | ] 44 | -------------------------------------------------------------------------------- /docusaurus/website/src/theme/DocSidebar/Desktop/CollapseButton/styles.module.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --docusaurus-collapse-button-bg: transparent; 3 | --docusaurus-collapse-button-bg-hover: rgb(0 0 0 / 10%); 4 | } 5 | 6 | [data-theme='dark']:root { 7 | --docusaurus-collapse-button-bg: rgb(255 255 255 / 5%); 8 | --docusaurus-collapse-button-bg-hover: rgb(255 255 255 / 10%); 9 | } 10 | 11 | @media (min-width: 997px) { 12 | .collapseSidebarButton { 13 | display: block !important; 14 | background-color: var(--docusaurus-collapse-button-bg); 15 | height: 40px; 16 | position: sticky; 17 | bottom: 0; 18 | border-radius: 0; 19 | border: 1px solid var(--ifm-toc-border-color); 20 | } 21 | 22 | .collapseSidebarButtonIcon { 23 | transform: rotate(180deg); 24 | margin-top: 4px; 25 | } 26 | 27 | [dir='rtl'] .collapseSidebarButtonIcon { 28 | transform: rotate(0); 29 | } 30 | 31 | .collapseSidebarButton:hover, 32 | .collapseSidebarButton:focus { 33 | background-color: var(--docusaurus-collapse-button-bg-hover); 34 | } 35 | } 36 | 37 | .collapseSidebarButton { 38 | display: none; 39 | margin: 0; 40 | } 41 | -------------------------------------------------------------------------------- /docusaurus/website/src/theme/DocSidebar/Desktop/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import {useThemeConfig} from '@docusaurus/theme-common'; 4 | import Logo from '@theme/Logo'; 5 | import CollapseButton from '@theme/DocSidebar/Desktop/CollapseButton'; 6 | import Content from '@theme/DocSidebar/Desktop/Content'; 7 | import type {Props} from '@theme/DocSidebar/Desktop'; 8 | 9 | import styles from './styles.module.css'; 10 | 11 | function DocSidebarDesktop({path, sidebar, onCollapse, isHidden}: Props) { 12 | const { 13 | navbar: {hideOnScroll}, 14 | docs: { 15 | sidebar: {hideable}, 16 | }, 17 | } = useThemeConfig(); 18 | 19 | return ( 20 |
26 | {hideOnScroll && } 27 | 28 | {hideable && } 29 |
30 | ); 31 | } 32 | 33 | export default React.memo(DocSidebarDesktop); 34 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Test and Example Data 2 | 3 | Unless correcting mistakes, do not change existing examples as this may break outside tests. (However, extending exampleInfo or updating the summary is permitted). 4 | 5 | The Directory format is `kind/format/version/collection/example_file.json`. 6 | 7 | Additional information for tests is kept in the custom meta data of the first frame in the "exampleInfo" property. 8 | 9 | Example File Requirements: 10 | 11 | - Must follow the directory format. 12 | - A json file that contains an array of frames (data.Frames) 13 | - Frames do not have a refId set. 14 | - The first frame must have meta.custom as an object, and have the "exampleInfo" property in it. 15 | - exampleInfo must contain: 16 | - "summary" (string) A description ending in a period. 17 | - "itemCount" (number) The number if items if a dimensional set based kind (e.g. numeric/timeseries). 18 | - "collectionVersion" (number) of at least 1. 19 | 20 | When new examples are added to a collection, they should be added with the max collectionVersion number within the collection incremented by one. Existing examples should not have their collectionVersion number changed. 21 | -------------------------------------------------------------------------------- /examples/data/timeseries/wide/v0.1/basic_valid/timeseries-wide_one-item-with-name-and-labels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "timeseries-wide", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "TimeseriesWide with 1 item. There is 1 item name (slothCount) and 1 dimension (city).", 10 | "itemCount": 1, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "name": "t", 20 | "type": "time", 21 | "typeInfo": { 22 | "frame": "time.Time" 23 | } 24 | }, 25 | { 26 | "name": "slothCount", 27 | "type": "number", 28 | "typeInfo": { 29 | "frame": "float64" 30 | }, 31 | "labels": { 32 | "city": "LGA" 33 | } 34 | } 35 | ] 36 | }, 37 | "data": { 38 | "values": [ 39 | [1664901845976, 1664902845976], 40 | [3, 5] 41 | ] 42 | } 43 | } 44 | ] 45 | -------------------------------------------------------------------------------- /examples/data/numeric/wide/v0.1/basic_valid/numeric-wide_two-empty-items.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "numeric-wide", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "NumericWide with 2 empty items. There is 1 item name (avgSlothCount) and 1 dimension (city).", 10 | "itemCount": 2, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "type": "number", 20 | "name": "avgSlothCount", 21 | "typeInfo": { 22 | "frame": "float64" 23 | }, 24 | "labels": { 25 | "city": "LGA" 26 | } 27 | }, 28 | { 29 | "type": "number", 30 | "name": "avgSlothCount", 31 | "typeInfo": { 32 | "frame": "float64" 33 | }, 34 | "labels": { 35 | "city": "MIA" 36 | } 37 | } 38 | ] 39 | }, 40 | "data": { 41 | "values": [[], []] 42 | } 43 | } 44 | ] 45 | -------------------------------------------------------------------------------- /examples/data/numeric/wide/v0.1/basic_valid/numeric-wide_two-items-by-dimension.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "numeric-wide", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "NumericWide with 2 items. 2 names (avgSlothCount, avgSleepHoursPerSlothPerDay) and 1 dimension (city).", 10 | "itemCount": 2, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "type": "number", 20 | "name": "avgSlothCount", 21 | "typeInfo": { 22 | "frame": "float64" 23 | }, 24 | "labels": { 25 | "city": "LGA" 26 | } 27 | }, 28 | { 29 | "type": "number", 30 | "name": "avgSlothCount", 31 | "typeInfo": { 32 | "frame": "float64" 33 | }, 34 | "labels": { 35 | "city": "MIA" 36 | } 37 | } 38 | ] 39 | }, 40 | "data": { 41 | "values": [[4], [7.5]] 42 | } 43 | } 44 | ] 45 | -------------------------------------------------------------------------------- /docusaurus/website/src/theme/DocSidebar/Desktop/CollapseButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import {translate} from '@docusaurus/Translate'; 4 | import IconArrow from '@theme/Icon/Arrow'; 5 | import type {Props} from '@theme/DocSidebar/Desktop/CollapseButton'; 6 | 7 | import styles from './styles.module.css'; 8 | 9 | export default function CollapseButton({onClick}: Props): JSX.Element { 10 | return ( 11 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /examples/data/numeric/wide/v0.1/basic_valid/numeric-wide_two-items-by-dimension-name.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "numeric-wide", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "NumericWide with 2 items. 2 names (avgSlothCount, avgSleepHoursPerSlothPerDay) and 1 dimension (city).", 10 | "itemCount": 2, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "type": "number", 20 | "name": "avgSlothCount", 21 | "typeInfo": { 22 | "frame": "float64" 23 | }, 24 | "labels": { 25 | "city": "LGA" 26 | } 27 | }, 28 | { 29 | "type": "number", 30 | "name": "avgSleepHoursPerSlothPerDay", 31 | "typeInfo": { 32 | "frame": "float64" 33 | }, 34 | "labels": { 35 | "city": "MIA" 36 | } 37 | } 38 | ] 39 | }, 40 | "data": { 41 | "values": [[4], [7.5]] 42 | } 43 | } 44 | ] 45 | -------------------------------------------------------------------------------- /sdata/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/grafana/dataplane/sdata 2 | 3 | go 1.22.4 4 | 5 | require ( 6 | github.com/google/go-cmp v0.6.0 7 | github.com/grafana/grafana-plugin-sdk-go v0.236.0 8 | github.com/stretchr/testify v1.9.0 9 | ) 10 | 11 | require ( 12 | github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect 13 | github.com/cheekybits/genny v1.0.0 // indirect 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/golang/snappy v0.0.3 // indirect 16 | github.com/google/flatbuffers v23.5.26+incompatible // indirect 17 | github.com/json-iterator/go v1.1.12 // indirect 18 | github.com/klauspost/compress v1.17.3 // indirect 19 | github.com/magefile/mage v1.15.0 // indirect 20 | github.com/mattetti/filebuffer v1.0.1 // indirect 21 | github.com/mattn/go-runewidth v0.0.9 // indirect 22 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 23 | github.com/modern-go/reflect2 v1.0.2 // indirect 24 | github.com/olekukonko/tablewriter v0.0.5 // indirect 25 | github.com/pierrec/lz4/v4 v4.1.18 // indirect 26 | github.com/pmezard/go-difflib v1.0.0 // indirect 27 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 28 | gopkg.in/yaml.v3 v3.0.1 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/grafana/dataplane/examples 2 | 3 | go 1.22.4 4 | 5 | require ( 6 | github.com/grafana/grafana-plugin-sdk-go v0.236.0 7 | github.com/stretchr/testify v1.9.0 8 | ) 9 | 10 | require ( 11 | github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect 12 | github.com/cheekybits/genny v1.0.0 // indirect 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/golang/snappy v0.0.3 // indirect 15 | github.com/google/flatbuffers v23.5.26+incompatible // indirect 16 | github.com/google/go-cmp v0.6.0 // indirect 17 | github.com/json-iterator/go v1.1.12 // indirect 18 | github.com/klauspost/compress v1.17.3 // indirect 19 | github.com/magefile/mage v1.15.0 // indirect 20 | github.com/mattetti/filebuffer v1.0.1 // indirect 21 | github.com/mattn/go-runewidth v0.0.9 // indirect 22 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 23 | github.com/modern-go/reflect2 v1.0.2 // indirect 24 | github.com/olekukonko/tablewriter v0.0.5 // indirect 25 | github.com/pierrec/lz4/v4 v4.1.18 // indirect 26 | github.com/pmezard/go-difflib v1.0.0 // indirect 27 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 28 | gopkg.in/yaml.v3 v3.0.1 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@grafana/dataplane", 3 | "version": "0.0.1", 4 | "description": "Grafana dataplane", 5 | "main": "dist/index.js", 6 | "private": true, 7 | "workspaces": [ 8 | "sdata", 9 | "docusaurus/website", 10 | "examples" 11 | ], 12 | "scripts": { 13 | "spellcheck": "cspell -c cspell.config.json \"**/*.{ts,tsx,js,go,md,mdx,yml,yaml,json,scss,css}\"", 14 | "docs": "turbo run docs:start", 15 | "docs:build": "turbo run docs:build", 16 | "docs:build:devportal:dev": "turbo docs:build -- --config=docusaurus.config.devportal.js", 17 | "docs:build:devportal:prod": "turbo docs:build -- --config=docusaurus.config.devportal.prod.js", 18 | "test:backend": "turbo run test:backend", 19 | "test": "turbo run test:backend" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/grafana/dataplane.git" 24 | }, 25 | "keywords": [], 26 | "author": "Grafana Labs", 27 | "license": "Apache-2.0", 28 | "bugs": { 29 | "url": "https://github.com/grafana/dataplane/issues" 30 | }, 31 | "homepage": "https://github.com/grafana/dataplane#readme", 32 | "packageManager": "yarn@1.22.22", 33 | "devDependencies": { 34 | "cspell": "8.9.1", 35 | "turbo": "2.0.5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/data/numeric/long/v0.1/basic_valid/numeric-long_empty-two-item-names.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "numeric-long", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "NumericLong with 0 empty items. There are 2 item names (avgSlothCount, avgSleepHoursPerSlothPerDay) and 1 dimension (city). But with 0 rows there are no items.", 10 | "itemCount": 0, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "type": "number", 20 | "name": "avgSlothCount", 21 | "typeInfo": { 22 | "frame": "float64" 23 | } 24 | }, 25 | { 26 | "type": "number", 27 | "name": "avgSleepHoursPerSlothPerDay", 28 | "typeInfo": { 29 | "frame": "float64" 30 | } 31 | }, 32 | { 33 | "type": "string", 34 | "name": "city", 35 | "typeInfo": { 36 | "frame": "string" 37 | } 38 | } 39 | ] 40 | }, 41 | "data": { 42 | "values": [] 43 | } 44 | } 45 | ] 46 | -------------------------------------------------------------------------------- /examples/data/timeseries/long/v0.1/basic_valid/timeseries-long_two-items-by-dimension.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "timeseries-long", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "TimeseriesLong with 2 items. 1 name (slothCount) and 1 dimension (city).", 10 | "itemCount": 2, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "name": "t", 20 | "type": "time", 21 | "typeInfo": { 22 | "frame": "time.Time" 23 | } 24 | }, 25 | { 26 | "type": "number", 27 | "name": "slothCount", 28 | "typeInfo": { 29 | "frame": "float64" 30 | } 31 | }, 32 | { 33 | "type": "string", 34 | "name": "city", 35 | "typeInfo": { 36 | "frame": "string" 37 | } 38 | } 39 | ] 40 | }, 41 | "data": { 42 | "values": [ 43 | [1664901845976, 1664901845976, 1664902845976, 1664902845976], 44 | [3, 6, 5, 9], 45 | ["LGA", "MIA", "LGA", "MIA"] 46 | ] 47 | } 48 | } 49 | ] 50 | -------------------------------------------------------------------------------- /sdata/numeric/util.go: -------------------------------------------------------------------------------- 1 | package numeric 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/grafana/dataplane/sdata" 7 | "github.com/grafana/grafana-plugin-sdk-go/data" 8 | ) 9 | 10 | func emptyFrameWithTypeMD(refID string, t data.FrameType, v data.FrameTypeVersion) *data.Frame { 11 | f := data.NewFrame("").SetMeta(&data.FrameMeta{Type: t, TypeVersion: v}) 12 | f.RefID = refID 13 | return f 14 | } 15 | 16 | func frameHasType(f *data.Frame, t data.FrameType) bool { 17 | return f != nil && f.Meta != nil && f.Meta.Type == t 18 | } 19 | 20 | func ignoreAdditionalFrames(reason string, frames []*data.Frame, ignored *[]sdata.FrameFieldIndex) (err error) { 21 | if len(frames) < 1 { 22 | return nil 23 | } 24 | for frameIdx, f := range (frames)[1:] { 25 | if f == nil { 26 | return fmt.Errorf("nil frame at %v which is invalid", frameIdx) 27 | } 28 | if len(f.Fields) == 0 { 29 | if ignored == nil { 30 | ignored = &([]sdata.FrameFieldIndex{}) 31 | } 32 | *ignored = append(*ignored, sdata.FrameFieldIndex{ 33 | FrameIdx: frameIdx + 1, FieldIdx: -1, Reason: reason}, 34 | ) 35 | } 36 | for fieldIdx := range frames { 37 | if ignored == nil { 38 | ignored = &([]sdata.FrameFieldIndex{}) 39 | } 40 | *ignored = append(*ignored, sdata.FrameFieldIndex{ 41 | FrameIdx: frameIdx + 1, FieldIdx: fieldIdx, Reason: reason}, 42 | ) 43 | } 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /examples/data/numeric/wide/v0.1/extended_valid/numeric-wide_two-items-by-dimension-with-remainder-time.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "numeric-wide", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "NumericWide with 2 items and a remainder time field. 1 name (avgSlothCount) and 1 dimension (city).", 10 | "itemCount": 2, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "name": "t", 20 | "type": "time", 21 | "typeInfo": { 22 | "frame": "time.Time" 23 | } 24 | }, 25 | { 26 | "type": "number", 27 | "name": "avgSlothCount", 28 | "typeInfo": { 29 | "frame": "float64" 30 | }, 31 | "labels": { 32 | "city": "LGA" 33 | } 34 | }, 35 | { 36 | "type": "number", 37 | "name": "avgSlothCount", 38 | "typeInfo": { 39 | "frame": "float64" 40 | }, 41 | "labels": { 42 | "city": "MIA" 43 | } 44 | } 45 | ] 46 | }, 47 | "data": { 48 | "values": [[1664901845976], [4], [7.5]] 49 | } 50 | } 51 | ] 52 | -------------------------------------------------------------------------------- /examples/data/timeseries/wide/v0.1/extended_valid/timeseries-wide_one-item-with-remainder-string.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "timeseries-wide", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "TimeseriesWide with 1 item and a remainder string field. There is 1 item name (slothCount) and 1 dimension (city).", 10 | "itemCount": 1, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "name": "t", 20 | "type": "time", 21 | "typeInfo": { 22 | "frame": "time.Time" 23 | } 24 | }, 25 | { 26 | "type": "number", 27 | "name": "slothCount", 28 | "typeInfo": { 29 | "frame": "float64" 30 | }, 31 | "labels": { 32 | "city": "LGA" 33 | } 34 | }, 35 | { 36 | "type": "string", 37 | "name": "remainder string field", 38 | "typeInfo": { 39 | "frame": "string" 40 | } 41 | } 42 | ] 43 | }, 44 | "data": { 45 | "values": [ 46 | [1664901845976, 1664902845976], 47 | [3, 5], 48 | ["remainder", "data"] 49 | ] 50 | } 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /examples/data/timeseries/wide/v0.1/extended_valid/timeseries-wide_one-item-with-remainder-time.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "timeseries-wide", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "TimeseriesWide with 1 item and a remainder time field. There is 1 item name (slothCount) and 1 dimension (city).", 10 | "itemCount": 1, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "name": "t", 20 | "type": "time", 21 | "typeInfo": { 22 | "frame": "time.Time" 23 | } 24 | }, 25 | { 26 | "type": "number", 27 | "name": "slothCount", 28 | "typeInfo": { 29 | "frame": "float64" 30 | }, 31 | "labels": { 32 | "city": "LGA" 33 | } 34 | }, 35 | { 36 | "name": "remainder time field", 37 | "type": "time", 38 | "typeInfo": { 39 | "frame": "time.Time" 40 | } 41 | } 42 | ] 43 | }, 44 | "data": { 45 | "values": [ 46 | [1664901845976, 1664902845976], 47 | [3, 5], 48 | [1764901845976, 1764902845976] 49 | ] 50 | } 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /examples/data/timeseries/wide/v0.1/basic_valid/timeseries-wide_two-items-by-dimension.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "timeseries-wide", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "TimeseriesWide with 2 items. There is 1 item name (slothCount) and 1 dimension (city).", 10 | "itemCount": 2, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "name": "t", 20 | "type": "time", 21 | "typeInfo": { 22 | "frame": "time.Time" 23 | } 24 | }, 25 | { 26 | "type": "number", 27 | "name": "slothCount", 28 | "typeInfo": { 29 | "frame": "float64" 30 | }, 31 | "labels": { 32 | "city": "LGA" 33 | } 34 | }, 35 | { 36 | "type": "number", 37 | "name": "slothCount", 38 | "typeInfo": { 39 | "frame": "float64" 40 | }, 41 | "labels": { 42 | "city": "MIA" 43 | } 44 | } 45 | ] 46 | }, 47 | "data": { 48 | "values": [ 49 | [1664901845976, 1664902845976], 50 | [3, 5], 51 | [6, 9] 52 | ] 53 | } 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /docusaurus/website/src/theme/prism.js: -------------------------------------------------------------------------------- 1 | const grafanaPrismTheme = { 2 | plain: { 3 | color: '#F8F8F2', 4 | backgroundColor: '#22252b', 5 | }, 6 | styles: [ 7 | { 8 | types: ['prolog', 'constant', 'builtin'], 9 | style: { 10 | color: '#6E9FFF', 11 | }, 12 | }, 13 | { 14 | types: ['inserted', 'function'], 15 | style: { 16 | color: '#6CCF8E', 17 | }, 18 | }, 19 | { 20 | types: ['deleted'], 21 | style: { 22 | color: '#FF5286', 23 | }, 24 | }, 25 | { 26 | types: ['changed'], 27 | style: { 28 | color: '#fbad37', 29 | }, 30 | }, 31 | { 32 | types: ['punctuation', 'symbol'], 33 | style: { 34 | color: 'rgb(204, 204, 220)', 35 | }, 36 | }, 37 | { 38 | types: ['string', 'char', 'tag', 'selector'], 39 | style: { 40 | color: '#FF5286', 41 | }, 42 | }, 43 | { 44 | types: ['keyword', 'variable'], 45 | style: { 46 | color: '#fbad37', 47 | fontStyle: 'italic', 48 | }, 49 | }, 50 | { 51 | types: ['comment'], 52 | style: { 53 | color: 'rgba(204, 204, 220, 0.65)', 54 | }, 55 | }, 56 | { 57 | types: ['attr-name'], 58 | style: { 59 | color: 'rgb(204, 204, 220)', 60 | }, 61 | }, 62 | ], 63 | }; 64 | 65 | module.exports = { 66 | grafanaPrismTheme, 67 | }; 68 | -------------------------------------------------------------------------------- /examples/data/timeseries/long/v0.1/basic_valid/timeseries-long_empty-two-item-names.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "timeseries-long", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "TimeSeriesLong with 0 empty items. There are 2 item names (avgSlothCount, avgSleepHoursPerSlothPerDay) and 1 dimension (city). But with 0 rows there are no items.", 10 | "itemCount": 0, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "name": "t", 20 | "type": "time", 21 | "typeInfo": { 22 | "frame": "time.Time" 23 | } 24 | }, 25 | { 26 | "type": "number", 27 | "name": "slothCount", 28 | "typeInfo": { 29 | "frame": "float64" 30 | } 31 | }, 32 | { 33 | "type": "number", 34 | "name": "sleepHoursPerSlothPerDay", 35 | "typeInfo": { 36 | "frame": "float64" 37 | } 38 | }, 39 | { 40 | "type": "string", 41 | "name": "city", 42 | "typeInfo": { 43 | "frame": "float64" 44 | } 45 | } 46 | ] 47 | }, 48 | "data": { 49 | "values": [] 50 | } 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /examples/data/numeric/long/v0.1/basic_valid/numeric-long_four-items-by-name-and-dimension.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "numeric-long", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "NumericLong with 4 items with 2 different item names (avgSlothCount, avgSleepHoursPerSlothPerDay) and 1 dimension (city).", 10 | "itemCount": 4, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "type": "string", 20 | "name": "city", 21 | "typeInfo": { 22 | "frame": "string" 23 | }, 24 | "labels": {}, 25 | "config": {} 26 | }, 27 | { 28 | "type": "number", 29 | "name": "avgSlothCount", 30 | "typeInfo": { 31 | "frame": "float64" 32 | }, 33 | "labels": {}, 34 | "config": {} 35 | }, 36 | { 37 | "type": "number", 38 | "name": "avgSleepHoursPerSlothPerDay", 39 | "typeInfo": { 40 | "frame": "float64" 41 | }, 42 | "labels": {}, 43 | "config": {} 44 | } 45 | ] 46 | }, 47 | "data": { 48 | "values": [ 49 | ["LGA", "MIA"], 50 | [1, 7.5], 51 | [23.5, 23.2] 52 | ] 53 | } 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /examples/data/numeric/long/v0.1/basic_valid/numeric-long_four-items-by-name-and-dimension-two-labels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "numeric-long", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "NumericLong with 4 items with 2 different item names and 2 dimensions (city and animal).", 10 | "itemCount": 4, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "type": "number", 20 | "name": "avgSlothCount", 21 | "typeInfo": { 22 | "frame": "float64" 23 | } 24 | }, 25 | { 26 | "type": "number", 27 | "name": "avgSleepHoursPerSlothPerDay", 28 | "typeInfo": { 29 | "frame": "float64" 30 | } 31 | }, 32 | { 33 | "type": "string", 34 | "name": "city", 35 | "typeInfo": { 36 | "frame": "string" 37 | } 38 | }, 39 | { 40 | "type": "string", 41 | "name": "animal", 42 | "typeInfo": { 43 | "frame": "string" 44 | } 45 | } 46 | ] 47 | }, 48 | "data": { 49 | "values": [ 50 | [4, 7.5], 51 | [23.5, 23.2], 52 | ["LGA", "MIA"], 53 | ["cat", "sloth"] 54 | ] 55 | } 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /docusaurus/website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@grafana/dataplane-website", 3 | "version": "0.7.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start --port 8080", 8 | "docs:start": "docusaurus start --port 8080", 9 | "build": "docusaurus build", 10 | "docs:build": "docusaurus build", 11 | "swizzle": "docusaurus swizzle", 12 | "deploy": "docusaurus deploy", 13 | "clear": "docusaurus clear", 14 | "serve": "docusaurus serve --port 8080", 15 | "write-translations": "docusaurus write-translations", 16 | "write-heading-ids": "docusaurus write-heading-ids", 17 | "typecheck": "tsc" 18 | }, 19 | "dependencies": { 20 | "@docusaurus/core": "2.4.0", 21 | "@docusaurus/preset-classic": "2.4.0", 22 | "@mdx-js/react": "^1.6.22", 23 | "clsx": "^1.2.1", 24 | "cookiejs": "^2.1.2", 25 | "docusaurus-lunr-search": "^2.4.1", 26 | "prism-react-renderer": "^1.3.5", 27 | "react": "^17.0.2", 28 | "react-dom": "^17.0.2" 29 | }, 30 | "devDependencies": { 31 | "@docusaurus/module-type-aliases": "2.4.0", 32 | "@tsconfig/docusaurus": "^1.0.5", 33 | "typescript": "^4.7.4" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.5%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "engines": { 48 | "node": ">=16" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docusaurus/website/src/theme/DocSidebar/Mobile/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { 4 | NavbarSecondaryMenuFiller, 5 | type NavbarSecondaryMenuComponent, 6 | ThemeClassNames, 7 | } from '@docusaurus/theme-common'; 8 | // @ts-ignore 9 | import {useNavbarMobileSidebar} from '@docusaurus/theme-common/internal'; 10 | import DocSidebarItems from '@theme/DocSidebarItems'; 11 | import type {Props} from '@theme/DocSidebar/Mobile'; 12 | 13 | // eslint-disable-next-line react/function-component-definition 14 | const DocSidebarMobileSecondaryMenu: NavbarSecondaryMenuComponent = ({ 15 | sidebar, 16 | path, 17 | }) => { 18 | const mobileSidebar = useNavbarMobileSidebar(); 19 | return ( 20 |
    21 | { 25 | // Mobile sidebar should only be closed if the category has a link 26 | if (item.type === 'category' && item.href) { 27 | mobileSidebar.toggle(); 28 | } 29 | if (item.type === 'link') { 30 | mobileSidebar.toggle(); 31 | } 32 | }} 33 | level={1} 34 | /> 35 |
36 | ); 37 | }; 38 | 39 | function DocSidebarMobile(props: Props) { 40 | return ( 41 | 45 | ); 46 | } 47 | 48 | export default React.memo(DocSidebarMobile); 49 | -------------------------------------------------------------------------------- /examples/data/timeseries/long/v0.1/basic_valid/timeseries-long_four-items-by-name-and-dimension.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "timeseries-long", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "TimeseriesLong with 4 items. 2 names (slothCount, sleepHoursPerSlothPerDay) and 1 dimension (city).", 10 | "itemCount": 4, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "name": "t", 20 | "type": "time", 21 | "typeInfo": { 22 | "frame": "time.Time" 23 | } 24 | }, 25 | { 26 | "type": "number", 27 | "name": "slothCount", 28 | "typeInfo": { 29 | "frame": "float64" 30 | } 31 | }, 32 | { 33 | "type": "number", 34 | "name": "sleepHoursPerSlothPerDay", 35 | "typeInfo": { 36 | "frame": "float64" 37 | } 38 | }, 39 | { 40 | "type": "string", 41 | "name": "city", 42 | "typeInfo": { 43 | "frame": "string" 44 | } 45 | } 46 | ] 47 | }, 48 | "data": { 49 | "values": [ 50 | [1664901845976, 1664901845976, 1664902845976, 1664902845976], 51 | [3, 6, 5, 9], 52 | [22, 23, 21.5, 23], 53 | ["LGA", "MIA", "LGA", "MIA"] 54 | ] 55 | } 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /examples/data/numeric/multi/v0.1/basic_valid/numeric-multi_two-items-by-dimension.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "numeric-multi", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "NumericMulti with 2 items. 1 name (avgSlothCount) and 1 dimension (city).", 10 | "itemCount": 2, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "type": "number", 20 | "name": "avgSlothCount", 21 | "typeInfo": { 22 | "frame": "float64" 23 | }, 24 | "labels": { 25 | "city": "LGA" 26 | } 27 | } 28 | ] 29 | }, 30 | "data": { 31 | "values": [[4]] 32 | } 33 | }, 34 | { 35 | "schema": { 36 | "meta": { 37 | "type": "numeric-multi", 38 | "typeVersion": [0, 1], 39 | "custom": { 40 | "exampleInfo": { 41 | "collectionVersion": 1, 42 | "valid": true, 43 | "noData": false 44 | } 45 | } 46 | }, 47 | "fields": [ 48 | { 49 | "type": "number", 50 | "name": "avgSlothCount", 51 | "typeInfo": { 52 | "frame": "float64" 53 | }, 54 | "labels": { 55 | "city": "MIA" 56 | } 57 | } 58 | ] 59 | }, 60 | "data": { 61 | "values": [[7.5]] 62 | } 63 | } 64 | ] 65 | -------------------------------------------------------------------------------- /docusaurus/website/src/utils/useOneTrustIntegration.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useCallback, useRef } from 'react'; 2 | import { 3 | loadOneTrustScript, 4 | hasConsent, 5 | onAnalyticsConsentChange, 6 | removeAnalyticsConsentChange, 7 | } from '../utils/oneTrustLoader'; 8 | 9 | /** 10 | * Hook that manages OneTrust cookie consent integration. 11 | * Loads OneTrust script, tracks consent state, and handles consent changes. 12 | */ 13 | export const useOneTrustIntegration = (oneTrustConfig) => { 14 | const [hasAnalyticsConsent, setHasAnalyticsConsent] = useState(false); 15 | 16 | const isRegistered = useRef(false); 17 | 18 | const handleConsentChange = useCallback((groupId, hasConsentValue) => { 19 | setHasAnalyticsConsent(hasConsentValue); 20 | }, []); 21 | 22 | useEffect(() => { 23 | if (!oneTrustConfig.enabled) { 24 | const localConsent = localStorage.getItem('localStorageConsent') === 'true'; 25 | setHasAnalyticsConsent(localConsent); 26 | return; 27 | } 28 | 29 | if (isRegistered.current) { 30 | return; 31 | } 32 | 33 | isRegistered.current = true; 34 | 35 | const existingConsent = hasConsent(); 36 | setHasAnalyticsConsent(existingConsent); 37 | 38 | onAnalyticsConsentChange(handleConsentChange, oneTrustConfig); 39 | 40 | const loaded = loadOneTrustScript(oneTrustConfig); 41 | 42 | return () => { 43 | removeAnalyticsConsentChange(handleConsentChange); 44 | isRegistered.current = false; 45 | }; 46 | }, [oneTrustConfig.enabled, handleConsentChange]); 47 | 48 | return { 49 | hasAnalyticsConsent, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /examples/data/numeric/multi/v0.1/basic_valid/numeric-multi_two-empty-items.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "numeric-multi", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "NumericMulti with 2 empty items. There is 1 item name (avgSlothCount) and 1 dimension (city). But with 0 rows there are no items.", 10 | "itemCount": 2, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "type": "number", 20 | "name": "avgSlothCount", 21 | "typeInfo": { 22 | "frame": "float64" 23 | }, 24 | "labels": { 25 | "city": "LGA" 26 | } 27 | } 28 | ] 29 | }, 30 | "data": { 31 | "values": [[]] 32 | } 33 | }, 34 | { 35 | "schema": { 36 | "meta": { 37 | "type": "numeric-multi", 38 | "typeVersion": [0, 1], 39 | "custom": { 40 | "exampleInfo": { 41 | "collectionVersion": 1, 42 | "valid": true, 43 | "noData": false 44 | } 45 | } 46 | }, 47 | "fields": [ 48 | { 49 | "type": "number", 50 | "name": "avgSlothCount", 51 | "typeInfo": { 52 | "frame": "float64" 53 | }, 54 | "labels": { 55 | "city": "MIA" 56 | } 57 | } 58 | ] 59 | }, 60 | "data": { 61 | "values": [[]] 62 | } 63 | } 64 | ] 65 | -------------------------------------------------------------------------------- /examples/data/numeric/multi/v0.1/basic_valid/numeric-multi_two-items-by-dimension-name-dif-name-dim.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "numeric-multi", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "NumericMulti with 2 items. 2 names (avgSlothCount, avgSleepHoursPerSlothPerDay) and 1 dimension (city).", 10 | "itemCount": 2, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "type": "number", 20 | "name": "avgSlothCount", 21 | "typeInfo": { 22 | "frame": "float64" 23 | }, 24 | "labels": { 25 | "city": "LGA" 26 | } 27 | } 28 | ] 29 | }, 30 | "data": { 31 | "values": [[4]] 32 | } 33 | }, 34 | { 35 | "schema": { 36 | "meta": { 37 | "type": "numeric-multi", 38 | "typeVersion": [0, 1], 39 | "custom": { 40 | "exampleInfo": { 41 | "collectionVersion": 1, 42 | "valid": true, 43 | "noData": false 44 | } 45 | } 46 | }, 47 | "fields": [ 48 | { 49 | "type": "number", 50 | "name": "avgSleepHoursPerSlothPerDay", 51 | "typeInfo": { 52 | "frame": "float64" 53 | }, 54 | "labels": { 55 | "city": "MIA" 56 | } 57 | } 58 | ] 59 | }, 60 | "data": { 61 | "values": [[7.5]] 62 | } 63 | } 64 | ] 65 | -------------------------------------------------------------------------------- /examples/data/numeric/wide/v0.1/basic_valid/numeric-wide_four-items-by-dimension-name.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "numeric-wide", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "NumericWide with 4 items with 2 different item names (avgSlothCount, avgSleepHoursPerSlothPerDay) and 1 dimension (city).", 10 | "itemCount": 4, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "type": "number", 20 | "name": "avgSlothCount", 21 | "typeInfo": { 22 | "frame": "float64" 23 | }, 24 | "labels": { 25 | "city": "LGA" 26 | } 27 | }, 28 | { 29 | "type": "number", 30 | "name": "avgSlothCount", 31 | "typeInfo": { 32 | "frame": "float64" 33 | }, 34 | "labels": { 35 | "city": "MIA" 36 | } 37 | }, 38 | { 39 | "type": "number", 40 | "name": "avgSleepHoursPerSlothPerDay", 41 | "typeInfo": { 42 | "frame": "float64" 43 | }, 44 | "labels": { 45 | "city": "LGA" 46 | } 47 | }, 48 | { 49 | "type": "number", 50 | "name": "avgSleepHoursPerSlothPerDay", 51 | "typeInfo": { 52 | "frame": "float64" 53 | }, 54 | "labels": { 55 | "city": "MIA" 56 | } 57 | } 58 | ] 59 | }, 60 | "data": { 61 | "values": [[4], [7.5], [23.5], [23.2]] 62 | } 63 | } 64 | ] 65 | -------------------------------------------------------------------------------- /docusaurus/website/src/theme/DocSidebar/Desktop/Content/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import clsx from 'clsx'; 3 | import {ThemeClassNames} from '@docusaurus/theme-common'; 4 | import { 5 | useAnnouncementBar, 6 | useScrollPosition, 7 | // @ts-ignore 8 | } from '@docusaurus/theme-common/internal'; 9 | import {translate} from '@docusaurus/Translate'; 10 | import DocSidebarItems from '@theme/DocSidebarItems'; 11 | import type {Props} from '@theme/DocSidebar/Desktop/Content'; 12 | 13 | import styles from './styles.module.css'; 14 | 15 | function useShowAnnouncementBar() { 16 | const {isActive} = useAnnouncementBar(); 17 | const [showAnnouncementBar, setShowAnnouncementBar] = useState(isActive); 18 | 19 | useScrollPosition( 20 | ({scrollY}) => { 21 | if (isActive) { 22 | setShowAnnouncementBar(scrollY === 0); 23 | } 24 | }, 25 | [isActive], 26 | ); 27 | return isActive && showAnnouncementBar; 28 | } 29 | 30 | export default function DocSidebarDesktopContent({ 31 | path, 32 | sidebar, 33 | className, 34 | }: Props): JSX.Element { 35 | const showAnnouncementBar = useShowAnnouncementBar(); 36 | 37 | return ( 38 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /sdata/common.go: -------------------------------------------------------------------------------- 1 | package sdata 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/grafana/grafana-plugin-sdk-go/data" 7 | ) 8 | 9 | func ValidValueFields() []data.FieldType { 10 | // TODO: not sure about bool (factor or value) 11 | return append(data.NumericFieldTypes(), []data.FieldType{data.FieldTypeBool, data.FieldTypeNullableBool}...) 12 | } 13 | 14 | // FrameFieldIndex is for referencing data that is not considered part of the metric data 15 | // when the data is valid. Reason states why the field was not part of the metric data. 16 | type FrameFieldIndex struct { 17 | FrameIdx int 18 | FieldIdx int // -1 means no fields 19 | Reason string // only meant for human consumption 20 | } 21 | 22 | type FrameFieldIndices []FrameFieldIndex 23 | 24 | func (f FrameFieldIndices) Len() int { 25 | return len(f) 26 | } 27 | 28 | func (f FrameFieldIndices) Less(i, j int) bool { 29 | return f[i].FrameIdx < f[j].FrameIdx 30 | } 31 | 32 | func (f FrameFieldIndices) Swap(i, j int) { 33 | f[i], f[j] = f[j], f[i] 34 | } 35 | 36 | type VersionWarning struct { 37 | DataVersion data.FrameTypeVersion 38 | LibraryVersion data.FrameTypeVersion 39 | DataType data.FrameType 40 | } 41 | 42 | func (vw *VersionWarning) Error() string { 43 | var newOld string 44 | switch { 45 | case vw.DataVersion.Greater(vw.LibraryVersion): 46 | newOld = "newer" 47 | case vw.DataVersion.Less(vw.LibraryVersion): 48 | newOld = "older" 49 | default: 50 | panic(fmt.Sprintf("VersionWarning created with equal versions data version %s and library version %s", vw.DataVersion, vw.LibraryVersion)) 51 | } 52 | return fmt.Sprintf("datatype %s version %s is %s than library version %s", vw.DataType, vw.DataVersion, newOld, vw.LibraryVersion) 53 | } 54 | 55 | func (vw *VersionWarning) DataNewer() bool { 56 | return vw.DataVersion.Greater(vw.LibraryVersion) 57 | } 58 | -------------------------------------------------------------------------------- /sdata/reader/play_test.go: -------------------------------------------------------------------------------- 1 | package reader_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/grafana/dataplane/sdata/numeric" 8 | "github.com/grafana/dataplane/sdata/reader" 9 | "github.com/grafana/dataplane/sdata/timeseries" 10 | "github.com/grafana/grafana-plugin-sdk-go/data" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestCanReadBasedOnMeta(t *testing.T) { 15 | t.Run("basic test", func(t *testing.T) { 16 | tsWideNoData, err := timeseries.NewWideFrame("A", timeseries.WideFrameVersionLatest) 17 | require.NoError(t, err) 18 | 19 | dt, err := reader.CanReadBasedOnMeta(tsWideNoData.Frames()) 20 | require.NoError(t, err) 21 | require.Equal(t, data.KindTimeSeries, dt.Kind()) 22 | 23 | nopeFrames := data.Frames{data.NewFrame("")} 24 | _, err = reader.CanReadBasedOnMeta(nopeFrames) 25 | require.Error(t, err) 26 | }) 27 | 28 | t.Run("read something", func(t *testing.T) { 29 | n, err := numeric.NewWideFrame("A", numeric.WideFrameVersionLatest) 30 | require.NoError(t, err) 31 | 32 | err = n.AddMetric("cpu", data.Labels{"host": "king_sloth"}, 5.3) 33 | require.NoError(t, err) 34 | err = n.AddMetric("cpu", data.Labels{"host": "queen_sloth"}, 5.2) 35 | require.NoError(t, err) 36 | 37 | frames := n.Frames() 38 | 39 | dt, err := reader.CanReadBasedOnMeta(frames) 40 | require.NoError(t, err) 41 | 42 | if dt.Kind() == data.KindNumeric { 43 | nr, err := numeric.CollectionReaderFromFrames(frames) 44 | require.NoError(t, err) 45 | 46 | c, err := nr.GetCollection(false) 47 | require.NoError(t, err) 48 | require.NoError(t, c.Warning) 49 | 50 | require.Len(t, c.Refs, 2) 51 | for _, ref := range c.Refs { 52 | _ = ref 53 | val, empty, err := ref.NullableFloat64Value() 54 | require.NoError(t, err) 55 | require.Equal(t, false, empty) 56 | fmt.Printf("%v %v %v\n", ref.GetMetricName(), ref.GetLabels().String(), *val) 57 | } 58 | } 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /examples/data/numeric/multi/v0.1/extended_valid/numeric-multi_two-items-by-dimension-with-remainder-time.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "numeric-multi", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "NumericMulti with 2 items and a remainder time field. 1 name (avgSlothCount) and 1 dimension (city).", 10 | "itemCount": 2, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "name": "t", 20 | "type": "time", 21 | "typeInfo": { 22 | "frame": "time.Time" 23 | } 24 | }, 25 | { 26 | "type": "number", 27 | "name": "avgSlothCount", 28 | "typeInfo": { 29 | "frame": "float64" 30 | }, 31 | "labels": { 32 | "city": "LGA" 33 | } 34 | } 35 | ] 36 | }, 37 | "data": { 38 | "values": [[1664901845976], [4]] 39 | } 40 | }, 41 | { 42 | "schema": { 43 | "meta": { 44 | "type": "numeric-multi", 45 | "typeVersion": [0, 1], 46 | "custom": { 47 | "exampleInfo": { 48 | "collectionVersion": 1, 49 | "valid": true, 50 | "noData": false 51 | } 52 | } 53 | }, 54 | "fields": [ 55 | { 56 | "name": "t", 57 | "type": "time", 58 | "typeInfo": { 59 | "frame": "time.Time" 60 | } 61 | }, 62 | { 63 | "type": "number", 64 | "name": "avgSlothCount", 65 | "typeInfo": { 66 | "frame": "float64" 67 | }, 68 | "labels": { 69 | "city": "MIA" 70 | } 71 | } 72 | ] 73 | }, 74 | "data": { 75 | "values": [[1664901845976], [7.5]] 76 | } 77 | } 78 | ] 79 | -------------------------------------------------------------------------------- /sdata/timeseries/long_test.go: -------------------------------------------------------------------------------- 1 | package timeseries_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | "github.com/grafana/dataplane/sdata/timeseries" 9 | "github.com/grafana/grafana-plugin-sdk-go/data" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestLongSeriesGetCollection(t *testing.T) { 14 | t.Run("basic", func(t *testing.T) { 15 | ls := timeseries.LongFrame{ 16 | data.NewFrame("", 17 | data.NewField("time", nil, []time.Time{time.UnixMilli(1), time.UnixMilli(1)}), 18 | data.NewField("host", nil, []string{"a", "b"}), 19 | data.NewField("iface", nil, []string{"eth0", "eth0"}), 20 | data.NewField("in_bytes", nil, []float64{1, 2}), 21 | data.NewField("out_bytes", nil, []int64{3, 4}), 22 | ).SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesLong, TypeVersion: data.FrameTypeVersion{0, 1}}), 23 | } 24 | 25 | c, err := ls.GetCollection(false) 26 | require.NoError(t, err) 27 | 28 | expectedRefs := []timeseries.MetricRef{ 29 | { 30 | ValueField: data.NewField("in_bytes", data.Labels{"host": "a", "iface": "eth0"}, []float64{1}), 31 | TimeField: data.NewField("time", nil, []time.Time{time.UnixMilli(1)}), 32 | }, 33 | { 34 | ValueField: data.NewField("in_bytes", data.Labels{"host": "b", "iface": "eth0"}, []float64{2}), 35 | TimeField: data.NewField("time", nil, []time.Time{time.UnixMilli(1)}), 36 | }, 37 | { 38 | ValueField: data.NewField("out_bytes", data.Labels{"host": "a", "iface": "eth0"}, []int64{3}), 39 | TimeField: data.NewField("time", nil, []time.Time{time.UnixMilli(1)}), 40 | }, 41 | { 42 | ValueField: data.NewField("out_bytes", data.Labels{"host": "b", "iface": "eth0"}, []int64{4}), 43 | TimeField: data.NewField("time", nil, []time.Time{time.UnixMilli(1)}), 44 | }, 45 | } 46 | 47 | require.Empty(t, c.RemainderIndices) // TODO more specific []x{} vs nil 48 | 49 | require.NoError(t, c.Warning) 50 | 51 | if diff := cmp.Diff(expectedRefs, c.Refs, data.FrameTestCompareOptions()...); diff != "" { 52 | require.FailNow(t, "mismatch (-want +got):\n", diff) 53 | } 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /examples/data/timeseries/multi/v0.1/basic_valid/timeseries-multi_two-items-by-dimension-unaligned-time.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "timeseries-multi", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "TimeseriesMulti with 2 items. There is 1 item name (slothCount) and 1 dimension (city). The timestamps are not aligned between the two series.", 10 | "itemCount": 2, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "name": "t", 20 | "type": "time", 21 | "typeInfo": { 22 | "frame": "time.Time" 23 | } 24 | }, 25 | { 26 | "type": "number", 27 | "name": "slothCount", 28 | "typeInfo": { 29 | "frame": "float64" 30 | }, 31 | "labels": { 32 | "city": "LGA" 33 | } 34 | } 35 | ] 36 | }, 37 | "data": { 38 | "values": [ 39 | [1664901845976, 1664902845976], 40 | [3, 5] 41 | ] 42 | } 43 | }, 44 | { 45 | "schema": { 46 | "meta": { 47 | "type": "timeseries-multi", 48 | "typeVersion": [0, 1], 49 | "custom": { 50 | "exampleInfo": { 51 | "valid": true, 52 | "noData": false 53 | } 54 | } 55 | }, 56 | "fields": [ 57 | { 58 | "name": "t", 59 | "type": "time", 60 | "typeInfo": { 61 | "frame": "time.Time" 62 | } 63 | }, 64 | { 65 | "type": "number", 66 | "name": "slothCount", 67 | "typeInfo": { 68 | "frame": "float64" 69 | }, 70 | "labels": { 71 | "city": "MIA" 72 | } 73 | } 74 | ] 75 | }, 76 | "data": { 77 | "values": [ 78 | [1664901855976, 1664902455976, 1664902855976], 79 | [6, 7, 9] 80 | ] 81 | } 82 | } 83 | ] 84 | -------------------------------------------------------------------------------- /.github/workflows/deploy-to-developer-portal-dev.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Developer Portal DEV Bucket 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | branch: 7 | description: "Which branch to use?" 8 | default: "main" 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | deploy: 15 | name: Deploy docs to Developer Portal Bucket 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read #Clone repo 19 | id-token: write #Authenticate with GCS 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | with: 24 | ref: ${{ github.event.inputs.branch }} 25 | fetch-depth: 0 26 | persist-credentials: false 27 | - name: Setup node 28 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 29 | with: 30 | node-version: '22' 31 | registry-url: 'https://registry.npmjs.org' 32 | cache: 'yarn' 33 | 34 | - name: Install dependencies 35 | run: yarn install --immutable --prefer-offline 36 | 37 | - name: Make docs the homepage of this subsite 38 | run: | 39 | rm -f ./docusaurus/website/src/pages/index.tsx 40 | sed -i '1s/^/---\nslug: \/\n---\n/' ./docs/contract/contract.md 41 | 42 | - name: Build documentation website 43 | run: yarn docs:build:devportal:dev 44 | 45 | - uses: grafana/shared-workflows/actions/login-to-gcs@64c35f1dffd024130947f485ed6a150edfe83d22 # v0.2.0 46 | id: login-to-gcs 47 | with: 48 | service_account: 'github-developer-portal-dev@grafanalabs-workload-identity.iam.gserviceaccount.com' 49 | bucket: 'staging-developer-portal' 50 | 51 | - name: 'Set up Cloud SDK' 52 | uses: 'google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a' 53 | 54 | - name: 'Deploy to Developer Portal Bucket' 55 | run: | 56 | gsutil -m rsync -r -d -c ./docusaurus/website/build/ gs://staging-developer-portal/dataplane 57 | -------------------------------------------------------------------------------- /.github/workflows/deploy-to-developer-portal-prod.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Developer Portal PROD Bucket 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - ".github/workflows/deploy-to-developer-portal-prod.yml" 9 | - "docusaurus/**" 10 | - "docs/**" 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | deploy: 17 | name: Deploy docs to Developer Portal Bucket 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: read #Clone repo 21 | id-token: write #Authenticate with GCS 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | with: 26 | persist-credentials: false 27 | 28 | - name: Setup node 29 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 30 | with: 31 | node-version: '22' 32 | registry-url: 'https://registry.npmjs.org' 33 | cache: 'yarn' 34 | 35 | - name: Install dependencies 36 | run: yarn install --immutable --prefer-offline 37 | 38 | #mac: sed -i '' '1s/^/---\nslug: \/\n---\n/' ./docs/contract/contract.md 39 | #linux: sed -i '1s/^/---\nslug: \/\n---\n/' ./docs/contract/contract.md 40 | - name: Make docs the homepage of this subsite 41 | run: | 42 | rm -f ./docusaurus/website/src/pages/index.tsx 43 | sed -i '1s/^/---\nslug: \/\n---\n/' ./docs/contract/contract.md 44 | 45 | - name: Build documentation website 46 | run: yarn docs:build:devportal:prod 47 | 48 | - uses: grafana/shared-workflows/actions/login-to-gcs@64c35f1dffd024130947f485ed6a150edfe83d22 # v0.2.0 49 | id: login-to-gcs 50 | with: 51 | service_account: 'github-developer-portal@grafanalabs-workload-identity.iam.gserviceaccount.com' 52 | bucket: 'grafana-developer-portal' 53 | 54 | - name: 'Set up Cloud SDK' 55 | uses: 'google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a' 56 | 57 | - name: 'Deploy to Developer Portal Bucket' 58 | run: | 59 | gsutil -m rsync -r -d -c ./docusaurus/website/build/ gs://grafana-developer-portal/dataplane 60 | -------------------------------------------------------------------------------- /sdata/timeseries/convert_test.go: -------------------------------------------------------------------------------- 1 | package timeseries_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | "github.com/grafana/dataplane/sdata/timeseries" 9 | "github.com/grafana/grafana-plugin-sdk-go/data" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestLongToMulti(t *testing.T) { 14 | t.Run("basic", func(t *testing.T) { 15 | ls := timeseries.LongFrame{ 16 | data.NewFrame("", 17 | data.NewField("time", nil, []time.Time{time.UnixMilli(1), time.UnixMilli(1)}), 18 | data.NewField("host", nil, []string{"a", "b"}), 19 | data.NewField("iface", nil, []string{"eth0", "eth0"}), 20 | data.NewField("in_bytes", nil, []float64{1, 2}), 21 | data.NewField("out_bytes", nil, []int64{3, 4}), 22 | ).SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesLong, TypeVersion: data.FrameTypeVersion{0, 1}}), 23 | } 24 | 25 | multiFrame, err := timeseries.LongToMulti(&ls) 26 | require.NoError(t, err) 27 | 28 | expectedFrames := timeseries.MultiFrame{ 29 | addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMulti, timeseries.MultiFrameVersionLatest), 30 | data.NewField("time", nil, []time.Time{time.UnixMilli(1)}), 31 | data.NewField("in_bytes", data.Labels{"host": "a", "iface": "eth0"}, []float64{1})), 32 | addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMulti, timeseries.MultiFrameVersionLatest), 33 | data.NewField("time", nil, []time.Time{time.UnixMilli(1)}), 34 | data.NewField("in_bytes", data.Labels{"host": "b", "iface": "eth0"}, []float64{2})), 35 | addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMulti, timeseries.MultiFrameVersionLatest), 36 | data.NewField("time", nil, []time.Time{time.UnixMilli(1)}), 37 | data.NewField("out_bytes", data.Labels{"host": "a", "iface": "eth0"}, []int64{3})), 38 | addFields(emptyFrameWithTypeMD(data.FrameTypeTimeSeriesMulti, timeseries.MultiFrameVersionLatest), 39 | data.NewField("time", nil, []time.Time{time.UnixMilli(1)}), 40 | data.NewField("out_bytes", data.Labels{"host": "b", "iface": "eth0"}, []int64{4})), 41 | } 42 | 43 | require.Equal(t, len(*multiFrame), len(expectedFrames)) 44 | for i := range *multiFrame { 45 | if diff := cmp.Diff((expectedFrames)[i], (*multiFrame)[i], data.FrameTestCompareOptions()...); diff != "" { 46 | require.FailNow(t, "mismatch (-want +got):\n", diff) 47 | } 48 | } 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /examples/data/timeseries/multi/v0.1/extended_valid/timeseries-multi_two-items-by-dimension-unaligned-time-with-remainder-string.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "timeseries-multi", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "TimeseriesMulti with 2 items and a remainder string field in one of the frames. There is 1 item name (slothCount) and 1 dimension (city). The timestamps are not aligned between the two series.", 10 | "itemCount": 2, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "name": "t", 20 | "type": "time", 21 | "typeInfo": { 22 | "frame": "time.Time" 23 | } 24 | }, 25 | { 26 | "type": "number", 27 | "name": "slothCount", 28 | "typeInfo": { 29 | "frame": "float64" 30 | }, 31 | "labels": { 32 | "city": "LGA" 33 | } 34 | }, 35 | { 36 | "type": "string", 37 | "name": "slothNote", 38 | "typeInfo": { 39 | "frame": "string" 40 | } 41 | } 42 | ] 43 | }, 44 | "data": { 45 | "values": [ 46 | [1664901845976, 1664902845976], 47 | [3, 5], 48 | ["", "Sloth 🦥"] 49 | ] 50 | } 51 | }, 52 | { 53 | "schema": { 54 | "meta": { 55 | "type": "timeseries-multi", 56 | "typeVersion": [0, 1], 57 | "custom": { 58 | "exampleInfo": { 59 | "valid": true, 60 | "noData": false 61 | } 62 | } 63 | }, 64 | "fields": [ 65 | { 66 | "name": "t", 67 | "type": "time", 68 | "typeInfo": { 69 | "frame": "time.Time" 70 | } 71 | }, 72 | { 73 | "type": "number", 74 | "name": "slothCount", 75 | "typeInfo": { 76 | "frame": "float64" 77 | }, 78 | "labels": { 79 | "city": "MIA" 80 | } 81 | } 82 | ] 83 | }, 84 | "data": { 85 | "values": [ 86 | [1664901855976, 1664902455976, 1664902855976], 87 | [6, 7, 9] 88 | ] 89 | } 90 | } 91 | ] 92 | -------------------------------------------------------------------------------- /docusaurus/website/src/theme/tracking/index.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | rudderanalytics: any; 4 | } 5 | } 6 | 7 | let rudderId; 8 | 9 | export function getRudderId() { 10 | return rudderId; 11 | } 12 | 13 | const isRudderstackSetup = (config: RudderStackTrackingConfig) => 14 | config && config.writeKey && config.url && config.sdkUrl; 15 | 16 | // Enqueue all rudderstack requests until it is loaded and consent is granted 17 | let rudderstack = {}; 18 | let rudderQueue = []; 19 | const rudderMethods = ['page', 'track', 'identify', 'reset']; 20 | for (const method of rudderMethods) { 21 | rudderstack[method] = (...args) => { 22 | rudderQueue.push([method].concat(args)); 23 | }; 24 | } 25 | 26 | export type RudderStackTrackingConfig = { 27 | url: string; 28 | writeKey: string; 29 | configUrl: string; 30 | sdkUrl: string; 31 | }; 32 | 33 | export function startTracking(config: RudderStackTrackingConfig) { 34 | if (isRudderstackSetup(config)) { 35 | const { writeKey, url, configUrl } = config; 36 | 37 | const initRudderstack = async () => { 38 | rudderId = await window.rudderanalytics.getAnonymousId(); 39 | 40 | window.rudderanalytics.load(writeKey, url, { configUrl }); 41 | rudderstack = window.rudderanalytics; 42 | 43 | // send any queued requests 44 | for (const [method, ...args] of rudderQueue) { 45 | rudderstack[method](...args); 46 | } 47 | // clean up afterwards so trackPage 48 | rudderQueue = []; 49 | }; 50 | 51 | const el = document.createElement('script'); 52 | el.async = true; 53 | el.src = config.sdkUrl; 54 | el.onload = initRudderstack; 55 | document.getElementsByTagName('head')[0].appendChild(el); 56 | } 57 | } 58 | 59 | export function trackPage() { 60 | // rudderstack automagically accesses all this but if it isn't loaded we need to 61 | // define it manually. 62 | if (rudderQueue) { 63 | const { href, pathname } = location; 64 | const canonicalLink = document.querySelector("link[rel='canonical']"); 65 | 66 | const properties = { 67 | url: canonicalLink ? canonicalLink.getAttribute('href') : href, 68 | path: pathname, 69 | referrer: document.referrer || '$direct', 70 | title: document.title, 71 | tab_url: href, 72 | }; 73 | // @ts-ignore we don't have the types. 74 | rudderstack.page(properties); 75 | } else { 76 | // @ts-ignore we don't have the types. 77 | rudderstack.page(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /sdata/reader/reader.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/grafana/dataplane/sdata" 7 | "github.com/grafana/dataplane/sdata/numeric" 8 | "github.com/grafana/dataplane/sdata/timeseries" 9 | "github.com/grafana/grafana-plugin-sdk-go/data" 10 | ) 11 | 12 | func supportedTypes() map[data.FrameType]map[data.FrameTypeVersion]struct{} { 13 | // Perhaps in JSON 14 | m := make(map[data.FrameType]map[data.FrameTypeVersion]struct{}, 6) 15 | 16 | add := func(t data.FrameType, versions []data.FrameTypeVersion) { 17 | m[t] = make(map[data.FrameTypeVersion]struct{}, len(versions)) 18 | for _, v := range versions { 19 | m[t][v] = struct{}{} 20 | } 21 | } 22 | 23 | add(data.FrameTypeTimeSeriesLong, timeseries.LongFrameVersions()) 24 | add(data.FrameTypeTimeSeriesWide, timeseries.WideFrameVersions()) 25 | add(data.FrameTypeTimeSeriesMulti, timeseries.MultiFrameVersions()) 26 | 27 | add(data.FrameTypeNumericLong, numeric.LongFrameVersions()) 28 | add(data.FrameTypeNumericWide, numeric.WideFrameVersions()) 29 | add(data.FrameTypeNumericMulti, numeric.MultiFrameVersions()) 30 | 31 | return m 32 | } 33 | 34 | func latestVersions() map[data.FrameType]data.FrameTypeVersion { 35 | return map[data.FrameType]data.FrameTypeVersion{ 36 | data.FrameTypeTimeSeriesLong: timeseries.LongFrameVersionLatest, 37 | data.FrameTypeTimeSeriesWide: timeseries.WideFrameVersionLatest, 38 | data.FrameTypeTimeSeriesMulti: timeseries.MultiFrameVersionLatest, 39 | 40 | data.FrameTypeNumericLong: numeric.LongFrameVersionLatest, 41 | data.FrameTypeNumericWide: numeric.WideFrameVersionLatest, 42 | data.FrameTypeNumericMulti: numeric.MultiFrameVersionLatest, 43 | } 44 | } 45 | 46 | // CanReadBasedOnMeta checks the first frame in Frames to see if it has a FrameType and TypeVersion supported 47 | // by the sdata libraries. 48 | func CanReadBasedOnMeta(f data.Frames) (data.FrameType, error) { 49 | if len(f) == 0 { 50 | return data.FrameTypeUnknown, fmt.Errorf("untyped no data response") // This needs some thought (Valid -- Need signal for it in sdata) 51 | } 52 | 53 | if f[0].Meta == nil { 54 | return data.FrameType(data.FrameTypeUnknown.Kind()), fmt.Errorf("missing metadata") 55 | } 56 | 57 | md := f[0].Meta 58 | 59 | fType := md.Type 60 | fTypeVersion := md.TypeVersion 61 | st := supportedTypes() 62 | 63 | typeToV, ok := st[fType] 64 | if !ok { 65 | return data.FrameTypeUnknown, fmt.Errorf("unsupported frame type %s", fType) 66 | } 67 | 68 | if _, ok := typeToV[fTypeVersion]; !ok { 69 | // This needs more attention, in particular to 0.x vs 1.x<->1.x 70 | return fType, &sdata.VersionWarning{DataVersion: fTypeVersion, 71 | LibraryVersion: latestVersions()[fType], 72 | DataType: fType} 73 | } 74 | 75 | return fType, nil 76 | } 77 | -------------------------------------------------------------------------------- /docusaurus/website/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import Link from "@docusaurus/Link"; 4 | import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; 5 | import Layout from "@theme/Layout"; 6 | import styles from "./index.module.css"; 7 | 8 | // function HomepageHeader() { 9 | // const { siteConfig } = useDocusaurusContext(); 10 | // return ( 11 | //
12 | //
13 | // 14 | //
15 | //
16 | //

{siteConfig.title}

17 | //

{siteConfig.tagline}

18 | //
19 | // 23 | // Get Started 24 | // 25 | //
26 | //
27 | //
28 | // ); 29 | // } 30 | 31 | // export default function Home(): JSX.Element { 32 | // const { siteConfig } = useDocusaurusContext(); 33 | // return ( 34 | // 35 | // 36 | // 37 | // ); 38 | // } 39 | 40 | function HomepageHeader() { 41 | const { siteConfig } = useDocusaurusContext(); 42 | return ( 43 |
49 |
50 |
51 |

57 | {siteConfig.title}{" "} 58 | 59 |

60 |

{siteConfig.tagline}

61 | 65 | Get Started 66 | 67 |
68 |
69 |
70 | ); 71 | } 72 | 73 | export default function Home() { 74 | const { siteConfig } = useDocusaurusContext(); 75 | return ( 76 | 80 | 81 | 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /sdata/numeric/numeric.go: -------------------------------------------------------------------------------- 1 | package numeric 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/grafana/dataplane/sdata" 7 | "github.com/grafana/grafana-plugin-sdk-go/data" 8 | ) 9 | 10 | type CollectionWriter interface { 11 | AddMetric(metricName string, l data.Labels, value interface{}) error 12 | SetMetricMD(metricName string, l data.Labels, fc data.FieldConfig) 13 | } 14 | 15 | type CollectionRW interface { 16 | CollectionWriter 17 | CollectionReader 18 | } 19 | 20 | type CollectionReader interface { 21 | GetCollection(validateData bool) (Collection, error) 22 | 23 | Frames() data.Frames 24 | } 25 | 26 | type MetricRef struct { 27 | ValueField *data.Field 28 | } 29 | 30 | func (n MetricRef) GetMetricName() string { 31 | if n.ValueField != nil { 32 | return n.ValueField.Name 33 | } 34 | return "" 35 | } 36 | 37 | func (n MetricRef) GetLabels() data.Labels { 38 | if n.ValueField != nil { 39 | return n.ValueField.Labels 40 | } 41 | return nil 42 | } 43 | 44 | // FloatValue returns the *float64 of the value, a bool that is 45 | // true if the value is empty (no field, or zero length field) 46 | // and an error if the field can not be converted to a *float64. 47 | func (n MetricRef) NullableFloat64Value() (*float64, bool, error) { 48 | if n.ValueField.Len() != 1 { 49 | return nil, true, nil 50 | } 51 | f, err := n.ValueField.NullableFloatAt(0) 52 | if err != nil { 53 | return nil, false, err 54 | } 55 | return f, false, nil 56 | } 57 | 58 | type Collection struct { 59 | RefID string 60 | Refs []MetricRef 61 | RemainderIndices []sdata.FrameFieldIndex // TODO: Currently not populated 62 | Warning error 63 | } 64 | 65 | func (c Collection) NoData() bool { 66 | return c.Refs != nil && len(c.Refs) == 0 67 | } 68 | 69 | func CollectionReaderFromFrames(frames []*data.Frame) (CollectionReader, error) { 70 | if len(frames) == 0 { 71 | return nil, fmt.Errorf("must be at least one frame") 72 | } 73 | 74 | firstFrame := frames[0] 75 | if firstFrame == nil { 76 | return nil, fmt.Errorf("nil frames are invalid") 77 | } 78 | if firstFrame.Meta == nil { 79 | return nil, fmt.Errorf("metadata missing from first frame, can not determine type") 80 | } 81 | 82 | mt := firstFrame.Meta.Type 83 | var tcr CollectionReader 84 | 85 | switch { 86 | case mt == data.FrameTypeNumericMulti: 87 | mfs := MultiFrame(frames) 88 | tcr = &mfs 89 | case mt == data.FrameTypeNumericLong: 90 | ls := LongFrame{firstFrame} 91 | tcr = &ls // TODO change to Frames for extra/ignored data? 92 | case mt == data.FrameTypeNumericWide: 93 | wfs := WideFrame{firstFrame} 94 | tcr = &wfs 95 | default: 96 | return nil, fmt.Errorf("unsupported time series type %q", mt) 97 | } 98 | return tcr, nil 99 | } 100 | -------------------------------------------------------------------------------- /sdata/timeseries/wide_test.go: -------------------------------------------------------------------------------- 1 | package timeseries_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | "github.com/grafana/dataplane/sdata/timeseries" 9 | "github.com/grafana/grafana-plugin-sdk-go/data" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestWideFrameAddMetric_ValidCases(t *testing.T) { 14 | t.Run("add two metrics", func(t *testing.T) { 15 | wf, err := timeseries.NewWideFrame("A", timeseries.WideFrameVersionLatest) 16 | require.NoError(t, err) 17 | 18 | err = wf.SetTime("time", []time.Time{time.UnixMilli(1), time.UnixMilli(2)}) 19 | require.NoError(t, err) 20 | 21 | err = wf.AddSeries("one", []float64{1, 2}, data.Labels{"host": "a"}, nil) 22 | require.NoError(t, err) 23 | 24 | err = wf.AddSeries("one", []float64{3, 4}, data.Labels{"host": "b"}, nil) 25 | require.NoError(t, err) 26 | 27 | expectedFrame := data.NewFrame("", 28 | data.NewField("time", nil, []time.Time{time.UnixMilli(1), time.UnixMilli(2)}), 29 | data.NewField("one", data.Labels{"host": "a"}, []float64{1, 2}), 30 | data.NewField("one", data.Labels{"host": "b"}, []float64{3, 4}), 31 | ).SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesWide, TypeVersion: data.FrameTypeVersion{0, 1}}) 32 | 33 | expectedFrame.RefID = "A" 34 | 35 | if diff := cmp.Diff(expectedFrame, (*wf)[0], data.FrameTestCompareOptions()...); diff != "" { 36 | require.FailNow(t, "mismatch (-want +got):\n%s\n", diff) 37 | } 38 | }) 39 | } 40 | 41 | func TestWideFrameSeriesGetMetricRefs(t *testing.T) { 42 | t.Run("two metrics from wide to multi", func(t *testing.T) { 43 | wf, err := timeseries.NewWideFrame("A", timeseries.WideFrameVersionLatest) 44 | require.NoError(t, err) 45 | 46 | err = wf.SetTime("time", []time.Time{time.UnixMilli(1), time.UnixMilli(2)}) 47 | require.NoError(t, err) 48 | 49 | err = wf.AddSeries("one", []float64{1, 2}, data.Labels{"host": "a"}, nil) 50 | require.NoError(t, err) 51 | 52 | err = wf.AddSeries("one", []float64{3, 4}, data.Labels{"host": "b"}, nil) 53 | require.NoError(t, err) 54 | 55 | c, err := wf.GetCollection(false) 56 | require.NoError(t, err) 57 | 58 | expectedRefs := []timeseries.MetricRef{ 59 | { 60 | ValueField: data.NewField("one", data.Labels{"host": "a"}, []float64{1, 2}), 61 | TimeField: data.NewField("time", nil, []time.Time{time.UnixMilli(1), time.UnixMilli(2)}), 62 | }, 63 | { 64 | ValueField: data.NewField("one", data.Labels{"host": "b"}, []float64{3, 4}), 65 | TimeField: data.NewField("time", nil, []time.Time{time.UnixMilli(1), time.UnixMilli(2)}), 66 | }, 67 | } 68 | 69 | require.Empty(t, c.RemainderIndices) // TODO more specific []x{} vs nil 70 | require.NoError(t, c.Warning) 71 | 72 | if diff := cmp.Diff(expectedRefs, c.Refs, data.FrameTestCompareOptions()...); diff != "" { 73 | require.FailNow(t, "mismatch (-want +got):\n%s\n", diff) 74 | } 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /sdata/numeric/wide.go: -------------------------------------------------------------------------------- 1 | package numeric 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/grafana/dataplane/sdata" 7 | "github.com/grafana/grafana-plugin-sdk-go/data" 8 | ) 9 | 10 | type WideFrame struct { 11 | *data.Frame 12 | } 13 | 14 | func (wf *WideFrame) Frames() data.Frames { 15 | return data.Frames{wf.Frame} 16 | } 17 | 18 | var WideFrameVersionLatest = WideFrameVersions()[len(WideFrameVersions())-1] 19 | 20 | func WideFrameVersions() []data.FrameTypeVersion { 21 | return []data.FrameTypeVersion{{0, 1}} 22 | } 23 | 24 | func NewWideFrame(refID string, v data.FrameTypeVersion) (*WideFrame, error) { 25 | if v.Greater(WideFrameVersionLatest) { 26 | return nil, fmt.Errorf("can not create WideFrame of version %s because it is newer than library version %v", v, WideFrameVersionLatest) 27 | } 28 | f := data.NewFrame("") 29 | f.RefID = refID 30 | f.SetMeta(&data.FrameMeta{Type: data.FrameTypeNumericWide, TypeVersion: v}) 31 | return &WideFrame{f}, nil 32 | } 33 | 34 | func (wf *WideFrame) AddMetric(metricName string, l data.Labels, value interface{}) error { 35 | fType := data.FieldTypeFor(value) 36 | if !fType.Numeric() { 37 | return fmt.Errorf("unsupported value type %T, must be numeric", value) 38 | } 39 | 40 | if wf == nil || wf.Frame == nil { 41 | return fmt.Errorf("zero frames when calling AddMetric must call NewWideFrame first") 42 | } 43 | 44 | field := data.NewFieldFromFieldType(fType, 1) 45 | field.Name = metricName 46 | field.Labels = l 47 | field.Set(0, value) 48 | wf.Fields = append(wf.Fields, field) 49 | 50 | return nil 51 | } 52 | 53 | func (wf *WideFrame) GetCollection(validateData bool) (Collection, error) { 54 | return validateAndGetRefsWide(wf, validateData) 55 | } 56 | 57 | // TODO: Update with current rules to match(ish) time series 58 | func validateAndGetRefsWide(wf *WideFrame, validateData bool) (Collection, error) { 59 | if validateData { 60 | panic("validateData option is not implemented") 61 | } 62 | 63 | var c Collection 64 | if wf == nil { 65 | return c, fmt.Errorf("frame is nil") 66 | } 67 | 68 | c.RefID = wf.RefID 69 | 70 | if !frameHasType(wf.Frame, data.FrameTypeNumericWide) { 71 | return c, fmt.Errorf("frame has wrong type, expected NumericWide but got %q", wf.Meta.Type) 72 | } 73 | 74 | if wf.Meta.TypeVersion != WideFrameVersionLatest { 75 | c.Warning = &sdata.VersionWarning{DataVersion: wf.Meta.TypeVersion, LibraryVersion: WideFrameVersionLatest, DataType: data.FrameTypeNumericWide} 76 | } 77 | 78 | if len(wf.Fields) == 0 { 79 | c.Refs = []MetricRef{} 80 | return c, nil // empty response 81 | } 82 | 83 | for _, field := range wf.Fields { 84 | if !field.Type().Numeric() { 85 | continue 86 | } 87 | c.Refs = append(c.Refs, MetricRef{field}) 88 | } 89 | return c, nil 90 | } 91 | 92 | func (wf *WideFrame) Validate() (isEmpty bool, errors []error) { 93 | panic("not implemented") 94 | } 95 | 96 | func (wf *WideFrame) SetMetricMD(metricName string, l data.Labels, fc data.FieldConfig) { 97 | panic("not implemented") 98 | } 99 | -------------------------------------------------------------------------------- /examples/examples_test.go: -------------------------------------------------------------------------------- 1 | package examples_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/grafana/dataplane/examples" 9 | "github.com/grafana/grafana-plugin-sdk-go/data" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestGetExamples(t *testing.T) { 14 | _, err := examples.GetExamples() 15 | require.NoError(t, err) 16 | } 17 | 18 | func TestExamplesSort(t *testing.T) { 19 | es, err := examples.GetExamples() 20 | require.NoError(t, err) 21 | 22 | numericExamples, err := es.Filter(examples.FilterOptions{Kind: data.KindNumeric}) 23 | require.NoError(t, err) 24 | 25 | numericExamples.Sort(examples.SortFrameTypeAsc) 26 | require.Equal(t, data.FrameTypeNumericLong, numericExamples.AsSlice()[0].Info().Type) 27 | 28 | numericExamples.Sort(examples.SortFrameTypeDesc) 29 | require.Equal(t, data.FrameTypeNumericWide, numericExamples.AsSlice()[0].Info().Type) 30 | } 31 | 32 | func TestValidExamples(t *testing.T) { 33 | examples, err := examples.GetExamples() 34 | 35 | require.NoError(t, err) 36 | 37 | t.Run("all sumaries must end in period", func(t *testing.T) { 38 | for _, e := range examples.AsSlice() { 39 | info := e.Info() 40 | require.True(t, strings.HasSuffix(info.Summary, "."), fmt.Sprintf("Summary: %q Path: %q", info.Summary, info.Path)) 41 | } 42 | }) 43 | t.Run("all sumaries must have collectionVersion >= 1", func(t *testing.T) { 44 | for _, e := range examples.AsSlice() { 45 | info := e.Info() 46 | require.GreaterOrEqual(t, e.Info().CollectionVersion, 1, info.Path) 47 | } 48 | }) 49 | 50 | t.Run("all frames have zero value (empty string) refID", func(t *testing.T) { 51 | for _, e := range examples.AsSlice() { 52 | for _, frame := range e.Frames("") { 53 | require.Empty(t, frame.RefID) 54 | } 55 | } 56 | }) 57 | } 58 | 59 | func TestExampleFramesMutation(t *testing.T) { 60 | examples, err := examples.GetExamples() 61 | s := examples.AsSlice() 62 | require.NoError(t, err) 63 | ft := s[0].Frames("")[0].Meta.Type 64 | require.NotEmpty(t, ft) 65 | 66 | frame := s[0].Frames("")[0] 67 | frame.Meta.Type = "sloth" 68 | newFT := s[0].Frames("")[0].Meta.Type 69 | require.Equal(t, ft, newFT) 70 | } 71 | 72 | func TestExamplesFilter(t *testing.T) { 73 | e, err := examples.GetExamples() 74 | require.NoError(t, err) 75 | 76 | t.Run("frametype and kind must match if not zero value", func(t *testing.T) { 77 | _, err := e.Filter(examples.FilterOptions{Type: data.FrameTypeTimeSeriesLong, Kind: data.KindNumeric}) 78 | require.Error(t, err) 79 | }) 80 | 81 | t.Run("version filter will error for no results for version with no match", func(t *testing.T) { 82 | _, err := e.Filter(examples.FilterOptions{Version: data.FrameTypeVersion{124, 1233}}) 83 | require.Error(t, err) 84 | }) 85 | 86 | t.Run("can filter to numeric long", func(t *testing.T) { 87 | numLongExamples, err := e.Filter(examples.FilterOptions{Type: data.FrameTypeNumericLong}) 88 | 89 | require.NoError(t, err) 90 | for _, e := range numLongExamples.AsSlice() { 91 | require.Equal(t, data.FrameTypeNumericLong, e.Info().Type) 92 | } 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /examples/examples_sort.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | ) 7 | 8 | func (e *Examples) Sort(sorters ...ExampleSortFunc) { 9 | sort.Slice(e.e, func(i int, j int) bool { 10 | for _, s := range sorters { 11 | equal, less := s((e.e)[i], (e.e)[j]) 12 | if equal { 13 | continue 14 | } 15 | return less 16 | } 17 | return false 18 | }) 19 | } 20 | 21 | type ExampleSortFunc func(i, j Example) (equal, less bool) 22 | 23 | func SortKindAsc(i, j Example) (equal, less bool) { 24 | a, b := string(i.info.Type.Kind()), string(j.info.Type.Kind()) 25 | if a != b { 26 | return false, a < b 27 | } 28 | return true, false 29 | } 30 | 31 | func SortKindDesc(i, j Example) (equal, less bool) { 32 | a, b := string(i.info.Type.Kind()), string(j.info.Type.Kind()) 33 | if a != b { 34 | return false, a > b 35 | } 36 | return true, false 37 | } 38 | 39 | func SortFrameTypeAsc(i, j Example) (equal, less bool) { 40 | a, b := string(i.info.Type), string(j.info.Type) 41 | if a != b { 42 | return false, a < b 43 | } 44 | return true, false 45 | } 46 | 47 | func SortFrameTypeDesc(i, j Example) (equal, less bool) { 48 | a, b := string(i.info.Type), string(j.info.Type) 49 | if a != b { 50 | return false, a > b 51 | } 52 | return true, false 53 | } 54 | 55 | func SortCollectionAsc(i, j Example) (equal, less bool) { 56 | a, b := i.info.CollectionName, j.info.CollectionName 57 | if a != b { 58 | return false, a < b 59 | } 60 | return true, false 61 | } 62 | 63 | func SortCollectionDesc(i, j Example) (equal, less bool) { 64 | a, b := i.info.CollectionName, j.info.CollectionName 65 | if a != b { 66 | return false, a > b 67 | } 68 | return true, false 69 | } 70 | 71 | func SortVersionAsc(i, j Example) (equal, less bool) { 72 | a, b := i.info.Version, j.info.Version 73 | if a != b { 74 | return false, a.Less(b) 75 | } 76 | return true, false 77 | } 78 | 79 | func SortVersionDesc(i, j Example) (equal, less bool) { 80 | a, b := i.info.Version, j.info.Version 81 | if a != b { 82 | return false, a.Greater(b) 83 | } 84 | return true, false 85 | } 86 | 87 | func SortCollectionVersionAsc(i, j Example) (equal, less bool) { 88 | a, b := i.info.CollectionVersion, j.info.CollectionVersion 89 | if a != b { 90 | return false, a < b 91 | } 92 | return true, false 93 | } 94 | 95 | func SortCollectionVersionDesc(i, j Example) (equal, less bool) { 96 | a, b := i.info.CollectionVersion, j.info.CollectionVersion 97 | if a != b { 98 | return false, a > b 99 | } 100 | return true, false 101 | } 102 | 103 | func SortPathAsc(i, j Example) (equal, less bool) { 104 | a, b := strings.Replace(i.info.Path, "/", "\x00", -1), 105 | strings.Replace(j.info.Path, "/", "\x00", -1) 106 | if a != b { 107 | return false, a < b 108 | } 109 | return true, false 110 | } 111 | 112 | func SortPathDesc(i, j Example) (equal, less bool) { 113 | a, b := strings.Replace(i.info.Path, "/", "\x00", -1), 114 | strings.Replace(j.info.Path, "/", "\x00", -1) 115 | if a != b { 116 | return false, a > b 117 | } 118 | return true, false 119 | } 120 | -------------------------------------------------------------------------------- /examples/data/numeric/multi/v0.1/basic_valid/numeric-multi_four-items-by-dimension-name.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "schema": { 4 | "meta": { 5 | "type": "numeric-multi", 6 | "typeVersion": [0, 1], 7 | "custom": { 8 | "exampleInfo": { 9 | "summary": "NumericMulti with 4 items with 2 different item names (avgSlothCount, avgSleepHoursPerSlothPerDay) and 1 dimension (city).", 10 | "itemCount": 4, 11 | "collectionVersion": 1, 12 | "valid": true, 13 | "noData": false 14 | } 15 | } 16 | }, 17 | "fields": [ 18 | { 19 | "type": "number", 20 | "name": "avgSlothCount", 21 | "typeInfo": { 22 | "frame": "float64" 23 | }, 24 | "labels": { 25 | "city": "LGA" 26 | } 27 | } 28 | ] 29 | }, 30 | "data": { 31 | "values": [[4]] 32 | } 33 | }, 34 | { 35 | "schema": { 36 | "meta": { 37 | "type": "numeric-multi", 38 | "typeVersion": [0, 1], 39 | "custom": { 40 | "exampleInfo": { 41 | "collectionVersion": 1, 42 | "valid": true, 43 | "noData": false 44 | } 45 | } 46 | }, 47 | "fields": [ 48 | { 49 | "type": "number", 50 | "name": "avgSlothCount", 51 | "typeInfo": { 52 | "frame": "float64" 53 | }, 54 | "labels": { 55 | "city": "MIA" 56 | } 57 | } 58 | ] 59 | }, 60 | "data": { 61 | "values": [[7.5]] 62 | } 63 | }, 64 | { 65 | "schema": { 66 | "meta": { 67 | "type": "numeric-multi", 68 | "typeVersion": [0, 1], 69 | "custom": { 70 | "exampleInfo": { 71 | "collectionVersion": 1, 72 | "valid": true, 73 | "noData": false 74 | } 75 | } 76 | }, 77 | "fields": [ 78 | { 79 | "type": "number", 80 | "name": "avgSleepHoursPerSlothPerDay", 81 | "typeInfo": { 82 | "frame": "float64" 83 | }, 84 | "labels": { 85 | "city": "LGA" 86 | } 87 | } 88 | ] 89 | }, 90 | "data": { 91 | "values": [[23.5]] 92 | } 93 | }, 94 | { 95 | "schema": { 96 | "meta": { 97 | "type": "numeric-multi", 98 | "typeVersion": [0, 1], 99 | "custom": { 100 | "exampleInfo": { 101 | "collectionVersion": 1, 102 | "valid": true, 103 | "noData": false 104 | } 105 | } 106 | }, 107 | "fields": [ 108 | { 109 | "type": "number", 110 | "name": "avgSleepHoursPerSlothPerDay", 111 | "typeInfo": { 112 | "frame": "float64" 113 | }, 114 | "labels": { 115 | "city": "MIA" 116 | } 117 | } 118 | ] 119 | }, 120 | "data": { 121 | "values": [[23.2]] 122 | } 123 | } 124 | ] 125 | -------------------------------------------------------------------------------- /docusaurus/website/src/theme/Root.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useCallback } from 'react'; 2 | import { useLocation } from '@docusaurus/router'; 3 | import { useOneTrustIntegration } from '../utils/useOneTrustIntegration'; 4 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 5 | 6 | import { CookieConsent } from '../components/CookieConsent/CookieConsent'; 7 | import { RudderStackTrackingConfig, startTracking, trackPage } from './tracking'; 8 | import { analyticsVersion, cookieName, getCookie, setCookie } from './tracking/cookie'; 9 | 10 | export default function Root({ children }) { 11 | const location = useLocation(); 12 | const { 13 | siteConfig: { customFields }, 14 | } = useDocusaurusContext(); 15 | 16 | const isOneTrustEnabled = customFields.oneTrust.enabled; 17 | 18 | const { hasAnalyticsConsent } = useOneTrustIntegration(customFields.oneTrust); 19 | 20 | const rudderStackConfig = customFields.rudderStackTracking as RudderStackTrackingConfig; 21 | 22 | const setCookieAndStartTracking = useCallback(() => { 23 | setCookie(cookieName, { 24 | analytics: analyticsVersion, 25 | }); 26 | 27 | setShouldShow(false); 28 | startTracking(rudderStackConfig); 29 | }, [rudderStackConfig]); 30 | 31 | const [shouldShow, setShouldShow] = useState(false); 32 | 33 | const canSpam = useCallback(async () => { 34 | try { 35 | const response = await fetch(customFields.canSpamUrl as string, { 36 | mode: 'no-cors', 37 | }); 38 | if (response.status === 204) { 39 | return true; 40 | } 41 | } catch (e) { 42 | // do nothing 43 | } 44 | return false; 45 | }, [customFields.canSpamUrl]); 46 | 47 | // Handles cookie consent logic when OneTrust is disabled 48 | const handleOriginalCookieConsent = useCallback(() => { 49 | // If the user has already given consent, start tracking. 50 | if (getCookie(cookieName, 'analytics') === analyticsVersion) { 51 | return setCookieAndStartTracking(); 52 | } 53 | 54 | // If the user is from an IP address that does not require consent, start tracking. 55 | canSpam() 56 | .then((result) => { 57 | if (result) { 58 | return setCookieAndStartTracking(); 59 | } else { 60 | // If the user has not given consent and is from IP address that requires consent, show the consent banner. 61 | setShouldShow(true); 62 | } 63 | }) 64 | .catch((error) => { 65 | console.error(error); 66 | setShouldShow(true); 67 | }); 68 | }, [setCookieAndStartTracking, canSpam]); 69 | 70 | useEffect(() => { 71 | if (isOneTrustEnabled) { 72 | return; 73 | } 74 | handleOriginalCookieConsent(); 75 | }, [isOneTrustEnabled, handleOriginalCookieConsent]); 76 | 77 | useEffect(() => { 78 | const shouldTrack = isOneTrustEnabled 79 | ? hasAnalyticsConsent 80 | : getCookie(cookieName, 'analytics') === analyticsVersion; 81 | 82 | if (shouldTrack) { 83 | if (isOneTrustEnabled && hasAnalyticsConsent) { 84 | startTracking(rudderStackConfig); 85 | } 86 | trackPage(); 87 | } 88 | }, [location, hasAnalyticsConsent, isOneTrustEnabled]); 89 | 90 | const onClick = () => { 91 | return setCookieAndStartTracking(); 92 | }; 93 | 94 | return ( 95 | <> 96 | {children} 97 | {!isOneTrustEnabled && shouldShow && } 98 | 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /sdata/timeseries/util.go: -------------------------------------------------------------------------------- 1 | package timeseries 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/grafana/dataplane/sdata" 8 | "github.com/grafana/grafana-plugin-sdk-go/data" 9 | ) 10 | 11 | func emptyFrameWithTypeMD(refID string, t data.FrameType, v data.FrameTypeVersion) *data.Frame { 12 | f := data.NewFrame("").SetMeta(&data.FrameMeta{Type: t, TypeVersion: v}) 13 | f.RefID = refID 14 | return f 15 | } 16 | 17 | func frameHasType(f *data.Frame, t data.FrameType) bool { 18 | return f != nil && f.Meta != nil && f.Meta.Type == t 19 | } 20 | 21 | func timeIsSorted(field *data.Field) (bool, error) { 22 | switch { 23 | case field == nil: 24 | return false, fmt.Errorf("field is nil") 25 | case field.Type() != data.FieldTypeTime: 26 | return false, fmt.Errorf("field is not a time field") 27 | case field.Len() == 0: 28 | return true, nil 29 | } 30 | 31 | for tIdx := 1; tIdx < field.Len(); tIdx++ { 32 | prevTime := field.At(tIdx - 1).(time.Time) 33 | curTime := field.At(tIdx).(time.Time) 34 | if curTime.Before(prevTime) { 35 | return false, nil 36 | } 37 | } 38 | return true, nil 39 | } 40 | 41 | // seriesTimeCheck checks that there is []time.Time field. 42 | // returns additional []time.Time fields. 43 | func seriesCheckSelectTime( 44 | frameIdx int, 45 | frame *data.Frame, 46 | ) (*data.Field, []sdata.FrameFieldIndex, error) { 47 | var ignoredFields []sdata.FrameFieldIndex 48 | 49 | timeFields := frame.TypeIndices(data.FieldTypeTime) 50 | 51 | // Must have []time.Time field (no nullable time) 52 | if len(timeFields) == 0 { 53 | return nil, nil, fmt.Errorf("frame %v is missing a []time.Time field", frameIdx) 54 | } 55 | 56 | if len(timeFields) > 1 { 57 | for _, fieldIdx := range timeFields[1:] { 58 | ignoredFields = append(ignoredFields, sdata.FrameFieldIndex{ 59 | FrameIdx: frameIdx, FieldIdx: fieldIdx, 60 | Reason: "additional time field"}) 61 | } 62 | } 63 | 64 | // Validate time Field is sorted in ascending (oldest to newest) order 65 | timeField := frame.Fields[timeFields[0]] 66 | 67 | return timeField, ignoredFields, nil 68 | } 69 | 70 | // malformedFrameCheck checks if there is a nil field in the slice frames or 71 | // if the fields are of unequal length 72 | func malformedFrameCheck(frameIdx int, frame *data.Frame) error { 73 | for fieldIdx, field := range frame.Fields { // TODO: frame.TypeIndices should do this 74 | if field == nil { 75 | return fmt.Errorf("frame %v has a nil field at %v", frameIdx, fieldIdx) 76 | } 77 | } 78 | if _, err := frame.RowLen(); err != nil { 79 | return fmt.Errorf("frame %v has mismatched field lengths: %w", frameIdx, err) 80 | } 81 | return nil 82 | } 83 | 84 | func ignoreAdditionalFrames(reason string, frames []*data.Frame, ignored *[]sdata.FrameFieldIndex) (err error) { 85 | if len(frames) < 1 { 86 | return nil 87 | } 88 | for frameIdx, f := range (frames)[1:] { 89 | if f == nil { 90 | return fmt.Errorf("nil frame at %v which is invalid", frameIdx) 91 | } 92 | if len(f.Fields) == 0 { 93 | if ignored == nil { 94 | ignored = &([]sdata.FrameFieldIndex{}) 95 | } 96 | *ignored = append(*ignored, sdata.FrameFieldIndex{ 97 | FrameIdx: frameIdx + 1, FieldIdx: -1, Reason: reason}, 98 | ) 99 | } 100 | for fieldIdx := range frames { 101 | if ignored == nil { 102 | ignored = &([]sdata.FrameFieldIndex{}) 103 | } 104 | *ignored = append(*ignored, sdata.FrameFieldIndex{ 105 | FrameIdx: frameIdx + 1, FieldIdx: fieldIdx, Reason: reason}, 106 | ) 107 | } 108 | } 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /docusaurus/website/src/utils/oneTrustLoader.js: -------------------------------------------------------------------------------- 1 | let oneTrustInjected = false; 2 | let consentChangeCallbacks = []; 3 | 4 | /** 5 | * Load OneTrust script if conditions are met 6 | * @param {Object} oneTrustConfig - OneTrust configuration object 7 | * @returns {boolean} - Whether OneTrust script was loaded 8 | */ 9 | export function loadOneTrustScript(oneTrustConfig) { 10 | // Check if user has already made a consent decision (either accept or reject) 11 | const consentDecision = localStorage.getItem('localStorageConsent'); 12 | if (consentDecision !== null) { 13 | // User has already made a decision, don't load OneTrust script again 14 | return consentDecision === 'true'; 15 | } 16 | 17 | if (!oneTrustConfig?.enabled) { 18 | return false; 19 | } 20 | 21 | if (oneTrustInjected || window.__oneTrustInjected) { 22 | return false; 23 | } 24 | 25 | try { 26 | const script = document.createElement('script'); 27 | script.type = 'text/javascript'; 28 | script.charset = 'UTF-8'; 29 | script.async = true; 30 | script.src = oneTrustConfig.scriptSrc; 31 | 32 | script.setAttribute('data-domain-script', oneTrustConfig.domainId); 33 | 34 | script.onload = function () { 35 | oneTrustInjected = true; 36 | window.__oneTrustInjected = true; 37 | }; 38 | 39 | script.onerror = function () { 40 | oneTrustInjected = false; 41 | window.__oneTrustInjected = false; 42 | }; 43 | 44 | document.head.appendChild(script); 45 | return true; 46 | } catch (error) { 47 | console.error('Error loading OneTrust script:', error); 48 | return false; 49 | } 50 | } 51 | 52 | /** 53 | * Check if user has given consent (simplified version) 54 | * @returns {boolean} 55 | */ 56 | export function hasConsent() { 57 | const hasLocalConsent = localStorage.getItem('localStorageConsent') === 'true'; 58 | return hasLocalConsent; 59 | } 60 | 61 | /** 62 | * Remove a callback from the analytics consent change listeners 63 | * @param callback - Function to remove from the callbacks list 64 | */ 65 | export function removeAnalyticsConsentChange(callback) { 66 | consentChangeCallbacks = consentChangeCallbacks.filter((cb) => cb !== callback); 67 | } 68 | 69 | /** 70 | * Register callback for analytics consent changes 71 | * @param callback - Function to call when consent changes 72 | * @param config - OneTrust configuration object 73 | */ 74 | export function onAnalyticsConsentChange(callback, config = null) { 75 | if (typeof window === 'undefined') return; 76 | 77 | let analyticsGroupId = 'C0002'; 78 | if (config?.analyticsGroupId) { 79 | analyticsGroupId = config.analyticsGroupId; 80 | } 81 | 82 | // Prevent duplicate callback registration 83 | if (!consentChangeCallbacks.includes(callback)) { 84 | consentChangeCallbacks.push(callback); 85 | } 86 | 87 | if (!window.OptanonWrapper) { 88 | window.OptanonWrapper = function () { 89 | const stack = new Error().stack; 90 | const isInitialLoad = stack.includes('windowLoadBanner'); 91 | 92 | if (isInitialLoad) { 93 | return; 94 | } 95 | 96 | const hasAnalyticsConsent = window.OnetrustActiveGroups && window.OnetrustActiveGroups.includes(analyticsGroupId); 97 | 98 | if (hasAnalyticsConsent) { 99 | localStorage.setItem('localStorageConsent', 'true'); 100 | } else { 101 | localStorage.setItem('localStorageConsent', 'false'); 102 | } 103 | 104 | consentChangeCallbacks.forEach((cb, index) => { 105 | try { 106 | cb('analytics', hasAnalyticsConsent); 107 | } catch (error) { 108 | console.error(`Error in consent change callback ${index + 1}:`, error); 109 | } 110 | }); 111 | }; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /docusaurus/website/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /sdata/numeric/long.go: -------------------------------------------------------------------------------- 1 | package numeric 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/grafana/dataplane/sdata" 8 | "github.com/grafana/grafana-plugin-sdk-go/data" 9 | ) 10 | 11 | type LongFrame struct { 12 | *data.Frame 13 | } 14 | 15 | func (lf *LongFrame) Frames() data.Frames { 16 | return data.Frames{lf.Frame} 17 | } 18 | 19 | var LongFrameVersionLatest = LongFrameVersions()[len(LongFrameVersions())-1] 20 | 21 | func LongFrameVersions() []data.FrameTypeVersion { 22 | return []data.FrameTypeVersion{{0, 1}} 23 | } 24 | 25 | func NewLongFrame(refID string, v data.FrameTypeVersion) (*LongFrame, error) { 26 | if v.Greater(LongFrameVersionLatest) { 27 | return nil, fmt.Errorf("can not create LongFrame of version %s because it is newer than library version %v", v, LongFrameVersionLatest) 28 | } 29 | return &LongFrame{emptyFrameWithTypeMD(refID, data.FrameTypeNumericLong, v)}, nil 30 | } 31 | 32 | func (lf *LongFrame) GetCollection(validateData bool) (Collection, error) { 33 | return validateAndGetRefsLong(lf, validateData) 34 | } 35 | 36 | // TODO: Update with current rules to match(ish) time series 37 | func validateAndGetRefsLong(lf *LongFrame, validateData bool) (Collection, error) { 38 | var c Collection 39 | if validateData { 40 | panic("validateData option is not implemented") 41 | } 42 | 43 | if lf == nil || lf.Frame == nil { 44 | return c, fmt.Errorf("nil frame is invalid") 45 | } 46 | 47 | c.RefID = lf.Frame.RefID 48 | 49 | if !frameHasType(lf.Frame, data.FrameTypeNumericLong) { 50 | return c, fmt.Errorf("frame is missing %s type indicator", data.FrameTypeNumericLong) 51 | } 52 | 53 | if lf.Meta.TypeVersion != LongFrameVersionLatest { 54 | c.Warning = &sdata.VersionWarning{DataVersion: lf.Meta.TypeVersion, LibraryVersion: LongFrameVersionLatest, DataType: data.FrameTypeNumericLong} 55 | } 56 | 57 | if len(lf.Fields) == 0 { // TODO: Error differently if nil and not zero length? 58 | c.Refs = []MetricRef{} 59 | return c, nil // empty response 60 | } 61 | 62 | stringFieldIds, numericFieldIds := []int{}, []int{} 63 | stringFieldNames, numericFieldNames := []string{}, []string{} 64 | 65 | for i, field := range lf.Fields { 66 | fType := field.Type() 67 | switch { 68 | case fType.Numeric(): 69 | numericFieldIds = append(numericFieldIds, i) 70 | numericFieldNames = append(numericFieldNames, field.Name) 71 | case fType == data.FieldTypeString || fType == data.FieldTypeNullableString: 72 | stringFieldIds = append(stringFieldIds, i) 73 | stringFieldNames = append(stringFieldNames, field.Name) 74 | } 75 | } 76 | 77 | for rowIdx := 0; rowIdx < lf.Rows(); rowIdx++ { 78 | l := data.Labels{} 79 | for i := range stringFieldIds { 80 | key := stringFieldNames[i] 81 | val, _ := lf.ConcreteAt(stringFieldIds[i], rowIdx) 82 | l[key] = val.(string) 83 | } 84 | 85 | for i, fieldIdx := range numericFieldIds { 86 | fType := lf.Fields[fieldIdx].Type() 87 | field := data.NewFieldFromFieldType(fType, 1) 88 | field.Name = numericFieldNames[i] 89 | field.Labels = l 90 | field.Set(0, lf.Fields[fieldIdx].At(rowIdx)) 91 | c.Refs = append(c.Refs, MetricRef{ 92 | ValueField: field, 93 | }) 94 | } 95 | } 96 | return c, nil 97 | } 98 | 99 | func SortNumericMetricRef(refs []MetricRef) { 100 | sort.SliceStable(refs, func(i, j int) bool { 101 | iRef := refs[i] 102 | jRef := refs[j] 103 | 104 | if iRef.GetMetricName() < jRef.GetMetricName() { 105 | return true 106 | } 107 | if iRef.GetMetricName() > jRef.GetMetricName() { 108 | return false 109 | } 110 | 111 | // If here Names are equal, next sort based on if there are labels. 112 | 113 | if iRef.GetLabels() == nil && jRef.GetLabels() == nil { 114 | return true // no labels first 115 | } 116 | if iRef.GetLabels() == nil && jRef.GetLabels() != nil { 117 | return true 118 | } 119 | if iRef.GetLabels() != nil && jRef.GetLabels() == nil { 120 | return false 121 | } 122 | 123 | return iRef.GetLabels().String() < jRef.GetLabels().String() 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /docs/img/logo.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /docs/contract/dataframes.md: -------------------------------------------------------------------------------- 1 | # Data frames 2 | 3 | A data frame is a collection of fields, where each field corresponds to a column. Each field, in turn, consists of a collection of values and metadata, such as the data type of those values. 4 | 5 | For example: 6 | 7 | ```ts 8 | export interface Field> { 9 | /** 10 | * Name of the field (column) 11 | */ 12 | name: string; 13 | /** 14 | * Field value type (string, number, and so on) 15 | */ 16 | type: FieldType; 17 | /** 18 | * Meta info about how field and how to display it 19 | */ 20 | config: FieldConfig; 21 | 22 | /** 23 | * The raw field values 24 | * In Grafana 10, this accepts both simple arrays and the Vector interface 25 | * In Grafana 11, the Vector interface has been removed 26 | */ 27 | values: V | T[]; 28 | 29 | /** 30 | * When type === FieldType.Time, this can optionally store 31 | * the nanosecond-precison fractions as integers between 32 | * 0 and 999999. 33 | */ 34 | nanos?: number[]; 35 | 36 | labels?: Labels; 37 | 38 | /** 39 | * Cached values with appropriate display and id values 40 | */ 41 | state?: FieldState | null; 42 | 43 | /** 44 | * Convert a value for display 45 | */ 46 | display?: DisplayProcessor; 47 | 48 | /** 49 | * Get value data links with variables interpolated 50 | */ 51 | getLinks?: (config: ValueLinkConfig) => Array>; 52 | } 53 | ``` 54 | 55 | ## Available data frame types 56 | 57 | A data frame type consists of a **kind** of data (for example, time series or numeric) and the data **format** (for example, wide). 58 | 59 | The following data frame types are available: 60 | 61 | - [Time series](./timeseries.md) 62 | - [Wide](./timeseries.md#time-series-wide-format-timeserieswide) 63 | - [Long](./timeseries.md#time-series-long-format-timeserieslong-sql-like) 64 | - [Multi](./timeseries.md#time-series-multi-format-timeseriesmulti) 65 | - [Numeric](./numeric.md) 66 | - [Wide](./numeric.md#numeric-wide-format-numericwide) 67 | - [Multi](./numeric.md#numeric-multi-format-numericmulti) 68 | - [Long](./numeric.md#numeric-many-format-numericlong) 69 | - [Heatmap](./heatmap.md) 70 | - [Rows](./heatmap.md#heatmap-rows-heatmaprows) 71 | - [Cells](./heatmap.md#heatmap-cells-heatmapcells) 72 | - [Logs](./logs.md) 73 | - [LogLines](./logs.md#loglines) 74 | 75 | ## Work with data frames 76 | 77 | Refer to [Data frames](https://grafana.com/developers/plugin-tools/key-concepts/data-frames) in the Plugins developer documentation for an overview of data frames. 78 | 79 | For a guide to plugin development with data frames, refer to [Create data frames](https://grafana.com/developers/plugin-tools/how-to-guides/data-source-plugins/create-data-frames). 80 | 81 | ## Technical references 82 | 83 | Data frames were introduced in Grafana 7.0, replacing the Time series and Table structures with a more generic data structure that can support a wider range of data types. The concept of data frame is borrowed from data analysis tools like the [R programming language](https://www.r-project.org) and [Pandas](https://pandas.pydata.org/). Other technical references are provided below. 84 | 85 | ### Apache Arrow 86 | 87 | The data frame structure is inspired by and uses the [Apache Arrow Project](https://arrow.apache.org/). JavaScript data frames use Arrow Tables as the underlying structure, and the backend Go code serializes its frames in Arrow Tables for transmission. 88 | 89 | ### JavaScript 90 | 91 | You can find the JavaScript implementation of data frames in the [`/src/dataframe` folder](https://github.com/grafana/grafana/tree/main/packages/grafana-data/src/dataframe) and [`/src/types/dataframe.ts`](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/types/dataFrame.ts) of the [`@grafana/data` package](https://github.com/grafana/grafana/tree/main/packages/grafana-data). 92 | 93 | ### Go 94 | 95 | For documentation on the Go implementation of data frames, refer to the [Grafana SDK Go data package](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/data?tab=doc). 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /docs/contract/logs.md: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | ## LogLines 4 | 5 | Version: 0.0 6 | 7 | ### Properties and field requirements 8 | 9 | - **Time field** - _required_ 10 | - The first time field with the name `timestamp` is the time field. 11 | - it must be non nullable 12 | - **Body field** - _required_ 13 | - The first string field with the name `body` is the body field. 14 | - it must be non nullable 15 | - **Severity field** - _optional_ 16 | - The first string field with the name `severity` is the severity field. 17 | - Represents the severity/level of the log line 18 | - If no severity field is found, consumers/client will decide the log level. Example: logs panel will try to parse the message field and determine the log level 19 | - Log level can be one of the values specified in the docs [here](https://grafana.com/docs/grafana/latest/explore/logs-integration/) 20 | - **ID field** - _optional_ 21 | - The first string field with the name `id` is the id field. 22 | - Unique identifier of the log line 23 | - **Labels field** - _optional_ 24 | - The first field with the name `labels` is the labels field. 25 | - This field represents additional information about the log line. 26 | - Field type must be json raw message type. Example value: `{}`, `{"hello":"world", "foo": 123.45, "bar" :["yellow","red"], "baz" : { "name": "alice" }}` 27 | - Value should be represented with `Record` type in javascript. 28 | 29 | Any other field is ignored by logs visualisation. 30 | 31 | ### Example 32 | 33 | Following is an example of a logs frame in go 34 | 35 | ```go 36 | data.NewFrame( 37 | "logs", 38 | data.NewField("timestamp", nil, []time.Time{time.UnixMilli(1645030244810), time.UnixMilli(1645030247027), time.UnixMilli(1645030247027)}), 39 | data.NewField("body", nil, []string{"message one", "message two", "message three"}), 40 | data.NewField("severity", nil, []string{"critical", "error", "warning"}), 41 | data.NewField("id", nil, []string{"xxx-001", "xyz-002", "111-003"}), 42 | data.NewField("labels", nil, []json.RawMessage{[]byte(`{}`), []byte(`{"hello":"world"}`), []byte(`{"hello":"world", "foo": 123.45, "bar" :["yellow","red"], "baz" : { "name": "alice" }}`)}), 43 | ) 44 | ``` 45 | 46 | the same can be represented as 47 | 48 | | Name: timestamp
Type: []time.Time | Name: body
Type: []string | Name: severity
Type: []\*string | Name: id
Type: []\*string | Name: labels
Type: []json.RawMessage | 49 | | --------------------------------------- | ------------------------------- | ------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------- | 50 | | 2022-02-16 16:50:44.810 +0000 GMT | message one | critical | xxx-001 | {} | 51 | | 2022-02-16 16:50:47.027 +0000 GMT | message two | error | xyz-002 | {"hello":"world"} | 52 | | 2022-02-16 16:50:47.027 +0000 GMT | message three | warning | 111-003 | {"hello":"world", "foo": 123.45, "bar" :["yellow","red"], "baz" : { "name": "alice" }} | 53 | 54 | ### Meta data requirements 55 | 56 | - Frame type must be set to `FrameTypeLogLines`/`log-lines` 57 | - Frame meta can optionally specify `preferredVisualisationType:logs` as meta data. Without this property, explore page will be rendering the logs data as table instead in logs view 58 | 59 | ### Invalid cases 60 | 61 | - Frame without time field 62 | - Frame without string field 63 | - Frame with field name "tsNs" where the type of the "tsNs" field is not number. 64 | 65 | ## Useful links 66 | 67 | - [OTel Logs Data Model](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md) 68 | - [OTel Logs Level](https://docs.google.com/document/d/1WQDz1jF0yKBXe3OibXWfy3g6lor9SvjZ4xT-8uuDCiA/edit#) 69 | - [Javascript high resolution timestamp](https://www.w3.org/TR/hr-time/) 70 | -------------------------------------------------------------------------------- /docs/contract/numeric.md: -------------------------------------------------------------------------------- 1 | # Numeric data frame kind 2 | 3 | Data frames of the kind _numeric_ are generally similar to their corresponding time series type, except that their value is a single number, instead of a series of (time, numeric value). 4 | 5 | In numeric kinds the value of each metric is a single number like 1, 2.3, or NaN. This generally corresponds to a Prometheus instant vector, or to a SQL table with string and number columns and multiple rows. 6 | 7 | ## Numeric Wide Format (NumericWide) 8 | 9 | Version: 0.1 10 | 11 | Example: 12 | 13 | 14 | 15 | 20 | 25 | 26 | 27 | 28 | 29 | 30 |
16 | Type: Number
17 | Name: cpu
18 | Labels: {"host": "a"} 19 |
21 | Type: Number
22 | Name: cpu
23 | Labels: {"host": "b"} 24 |
16
31 | 32 | Properties: 33 | 34 | - There should only be one frame with the type indicator 35 | - There should be no rows or a single row in the frame 36 | - All fields should have a numeric or bool type (e.g. if Go float64, \*int, etc) 37 | - Field Labels are used 38 | 39 | Remainder Data: 40 | 41 | - Any additional frames without the type indicator or a different one 42 | - Any time or string fields 43 | 44 | ## Numeric Multi Format (NumericMulti) 45 | 46 | Version: 0.1 47 | 48 | This logically is no different than NumericWide, except that instead of having one frame with multiple Fields there are multiple frames with a single field. 49 | 50 | **Example:** 51 | 52 | Frame 0: 53 | 54 | 55 | 56 | 61 | 62 | 63 | 64 | 65 |
57 | Type: Number
58 | Name: cpu
59 | Labels: {"host": "a"} 60 |
1
66 | 67 | Frame 1: 68 | 69 | 70 | 71 | 76 | 77 | 78 | 79 | 80 |
72 | Type: Number
73 | Name: cpu
74 | Labels: {"host": "b"} 75 |
6
81 | 82 | Properties: 83 | 84 | - There should be no rows or a single row in the frame 85 | - There should be one value field per frame 86 | 87 | Remainder Data: 88 | 89 | - Any time or string fields 90 | - Any value fields after the first 91 | - Any additional frames without the type indicator 92 | 93 | ## Numeric Long Format (NumericLong) [SQL-Table-Like] 94 | 95 | Version: 0.1 96 | 97 | This is the response one would imagine with a query like `Select Host, avg(cpu) … group by host". This is similar to the TimeSeriesLong format in that dimensions exist in string columns[^9]. 98 | 99 | Example: 100 | 101 | 102 | 103 | 108 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 |
104 | Type: Number
105 | Name: cpu
106 | Labels: nil 107 |
109 | Type: String
110 | Name: host
111 | Labels: nil 112 |
1a
6b
123 | 124 | Properties: 125 | 126 | - There should be a single Frame 127 | - There may be one or more value fields 128 | - If there is more than one row there needs to be one or more string fields 129 | - Each string column is a dimension, where the field/field name is the name of the dimension, and the corresponding values of the field are the dimensions value (e.g. a field with the name "host" would create a dimension like "host=web1" for a row/value in that field containing "web1" 130 | - The Labels property of each Field is unused 131 | - For each value field, the unique combination of item name (value Field Name) and its set of key (String field Name) and value (string field values) pairs form each unique item identifier. 132 | 133 | Remainder Data 134 | 135 | - Any additional frames with a different or no type indicator 136 | - Any time fields 137 | 138 | 139 | 140 | ## Notes 141 | 142 | [^9]: Other than this connection, "Long" is perhaps a bad name in this context, numeric table perhaps? 143 | -------------------------------------------------------------------------------- /docs/contract/heatmap.md: -------------------------------------------------------------------------------- 1 | # Heatmap 2 | 3 | Heatmaps are used to show the magnitude of a phenomenon as color in two dimensions. The variation in color may give visual cues about how the phenomenon is clustered or varies over space. 4 | 5 | ## Heatmap Rows (HeatmapRows) 6 | 7 | Version: 0.0 8 | 9 | The first field represents the X axis, the rest of the fields indicate rows in the heatmap. 10 | The true numeric range of each row can be indicated using an "le" label. When absent, 11 | The field display is used for the row label. 12 | 13 | Example: 14 | 15 | 16 | 17 | 21 | 26 | 31 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
18 | Type: Time
19 | Name: Time 20 |
22 | Type: Number
23 | Name:
24 | Labels: {"le": "10"} 25 |
27 | Type: Number
28 | Name:
29 | Labels: {"le": "20"} 30 |
32 | Type: Number
33 | Name:
34 | Labels: {"le": "+Inf"} 35 |
2022-05-24 18:19:51678
2022-05-24 18:19:51678
2022-05-24 18:19:51678
56 | 57 | Note: [Timeseries wide](./timeseries.md#time-series-wide-format-timeserieswide) can be used directly 58 | as heatmap-rows, in this case each value field becomes a row in the heatmap. 59 | 60 | ## Heatmap cells (HeatmapCells) 61 | 62 | Version: 0.0 63 | 64 | In this format, each row in the frame indicates the value of a single cell in a heatmap. 65 | There exists a row for every cell in the heatmap. 66 | 67 | **Example:** 68 | 69 | 70 | 71 | 75 | 79 | 83 | 87 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 |
72 | Type: Time
73 | Name: xMax|xMin|x 74 |
76 | Type: Number
77 | Name: yMax|yMin|y 78 |
80 | Type: Number
81 | Name: Count 82 |
84 | Type: Number
85 | Name: Total 86 |
88 | Type: Number
89 | Name: Speed 90 |
2022-05-24 18:19:51100111
2022-05-24 18:19:51200222
2022-05-24 18:19:51300333
2022-05-24 18:19:52100444
2022-05-24 18:19:52200555
2022-05-24 18:19:52300666
135 | 136 | This format requires uniform cell sizing. The size of the cell is defined by the columns in each row that are chosen as the xMax|xMin|x and the yMax|yMin|y. We can see that the Number column(yMax|yMin|y) increases by 100(each cell is roughly 100 higher than the previous cell on the y axis) for each row containing a similar Time value(these stacked cells all have roughly the same location along the x axis). This produces a uniform cell size. 137 | 138 | Note that multiple "value" fields can included to represent multiple dimensions within the same cell. 139 | The first value field is used in the display, unless explicitly configured 140 | 141 | The field names for yMax|yMin|y indicate the aggregation period or the supplied values. 142 | 143 | - yMax: the values are from the bucket below 144 | - yMin: the values are from to bucket above 145 | - y: the values are in the middle of the bucket 146 | 147 | ### Sparse heatmaps 148 | 149 | When both min+max fields exist for X and/or Y, the dimension does not need to be uniformly distributed. For high resolution with many gaps this approach can be smaller and more efficient. 150 | -------------------------------------------------------------------------------- /sdata/numeric/multi.go: -------------------------------------------------------------------------------- 1 | package numeric 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/grafana/dataplane/sdata" 7 | "github.com/grafana/grafana-plugin-sdk-go/data" 8 | ) 9 | 10 | type MultiFrame []*data.Frame 11 | 12 | func (mf *MultiFrame) Frames() data.Frames { 13 | return data.Frames(*mf) 14 | } 15 | 16 | var MultiFrameVersionLatest = MultiFrameVersions()[len(MultiFrameVersions())-1] 17 | 18 | func MultiFrameVersions() []data.FrameTypeVersion { 19 | return []data.FrameTypeVersion{{0, 1}} 20 | } 21 | 22 | func NewMultiFrame(refID string, v data.FrameTypeVersion) (*MultiFrame, error) { 23 | if v.Greater(MultiFrameVersionLatest) { 24 | return nil, fmt.Errorf("can not create MultiFrame of version %s because it is newer than library version %v", v, MultiFrameVersionLatest) 25 | } 26 | return &MultiFrame{ 27 | emptyFrameWithTypeMD(refID, data.FrameTypeNumericMulti, v), 28 | }, nil 29 | } 30 | 31 | func (mf *MultiFrame) AddMetric(metricName string, l data.Labels, value interface{}) error { 32 | fType := data.FieldTypeFor(value) 33 | if !fType.Numeric() { 34 | return fmt.Errorf("unsupported values type %T, must be numeric", value) 35 | } 36 | if mf == nil || len(*mf) == 0 { 37 | return fmt.Errorf("zero frames when calling AddMetric must call NewMultiFrame first") 38 | } 39 | 40 | field := data.NewFieldFromFieldType(fType, 1) 41 | field.Name = metricName 42 | field.Labels = l 43 | field.Set(0, value) 44 | 45 | if len(*mf) == 1 && len((*mf)[0].Fields) == 0 { 46 | (*mf)[0].Fields = append((*mf)[0].Fields, field) 47 | return nil 48 | } 49 | 50 | *mf = append(*mf, data.NewFrame("", field).SetMeta(&data.FrameMeta{ 51 | Type: data.FrameTypeNumericMulti, 52 | TypeVersion: (*mf)[0].Meta.TypeVersion, 53 | })) 54 | 55 | return nil 56 | } 57 | 58 | func (mf *MultiFrame) GetCollection(validateData bool) (Collection, error) { 59 | return validateAndGetRefsMulti(mf, validateData) 60 | } 61 | 62 | func (mf *MultiFrame) SetMetricMD(metricName string, l data.Labels, fc data.FieldConfig) { 63 | panic("not implemented") 64 | } 65 | 66 | /* 67 | Rules: 68 | - Whenever an error is returned, there are no ignored fields returned 69 | - Must have at least one frame 70 | - The first frame must be valid or will error, additional invalid frames with the type indicator will error, 71 | frames without type indicator are ignored 72 | - A valid individual Frame (in the non empty case) has a numeric field and a type indicator 73 | - Any nil Frames or Fields will cause an error (e.g. [Frame, Frame, nil, Frame] or [nil]) 74 | - If any frame has fields within the frame of different lengths, an error will be returned 75 | - If validateData is true, duplicate metricName+Labels will error 76 | - If all frames and their fields are ignored, and it is not the empty response case, an error is returned 77 | 78 | Things to decide: 79 | - Seems like allowing (ignoring) more than 1 row is not a good idea (outside of Long) 80 | - Will allow for extra frames 81 | 82 | TODO: Change this to follow the above 83 | */ 84 | func validateAndGetRefsMulti(mf *MultiFrame, validateData bool) (Collection, error) { 85 | if validateData { 86 | panic("validateData option is not implemented") 87 | } 88 | 89 | var c Collection 90 | if mf == nil { 91 | return c, fmt.Errorf("frame collection is nil") 92 | } 93 | 94 | if len(*mf) == 0 { 95 | return c, fmt.Errorf("must be at least one frame") 96 | } 97 | 98 | firstFrame := (*mf)[0] 99 | 100 | if firstFrame == nil { 101 | return c, fmt.Errorf("frame 0 is nil which is invalid") 102 | } 103 | 104 | if firstFrame.Meta == nil { 105 | return c, fmt.Errorf("frame 0 is missing a type indicator") 106 | } 107 | 108 | if len(firstFrame.Fields) == 0 { 109 | if len(*mf) > 1 { 110 | if err := ignoreAdditionalFrames("extra frame on empty response", *mf, &c.RemainderIndices); err != nil { 111 | return c, err 112 | } 113 | } 114 | // Empty Response 115 | c.Refs = []MetricRef{} 116 | return c, nil 117 | } 118 | 119 | c.RefID = (*mf)[0].RefID 120 | 121 | for _, frame := range *mf { 122 | if !frameHasType(frame, data.FrameTypeNumericMulti) { 123 | return c, fmt.Errorf("frame has wrong type, expected NumericMulti but got %q", frame.Meta.Type) 124 | } 125 | 126 | if frame.Meta.TypeVersion != MultiFrameVersionLatest { 127 | c.Warning = &sdata.VersionWarning{DataVersion: frame.Meta.TypeVersion, LibraryVersion: MultiFrameVersionLatest, DataType: data.FrameTypeNumericMulti} 128 | } 129 | 130 | valueFields := frame.TypeIndices(sdata.ValidValueFields()...) 131 | if len(valueFields) == 0 { 132 | continue 133 | } 134 | c.Refs = append(c.Refs, MetricRef{frame.Fields[valueFields[0]]}) 135 | } 136 | return c, nil 137 | } 138 | -------------------------------------------------------------------------------- /sdata/timeseries/series_test.go: -------------------------------------------------------------------------------- 1 | package timeseries_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/grafana/dataplane/sdata/timeseries" 8 | "github.com/grafana/grafana-plugin-sdk-go/data" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestSeriesCollectionReaderInterface(t *testing.T) { 13 | timeSlice := []time.Time{time.Unix(1234567890, 0), time.Unix(1234567891, 0)} 14 | 15 | metricName := "os.cpu" 16 | valuesA := []float64{1, 2} 17 | valuesB := []float64{3, 4} 18 | 19 | // refs should be same across the formats 20 | expectedRefs := []timeseries.MetricRef{ 21 | { 22 | data.NewField("time", nil, timeSlice), 23 | data.NewField(metricName, data.Labels{"host": "a"}, valuesA), 24 | }, 25 | { 26 | data.NewField("time", nil, timeSlice), 27 | data.NewField(metricName, data.Labels{"host": "b"}, valuesB), 28 | }, 29 | } 30 | 31 | t.Run("multi frame", func(t *testing.T) { 32 | sc, err := timeseries.NewMultiFrame("A", timeseries.WideFrameVersionLatest) 33 | require.NoError(t, err) 34 | 35 | err = sc.AddSeries(metricName, data.Labels{"host": "a"}, timeSlice, valuesA) 36 | require.NoError(t, err) 37 | 38 | err = sc.AddSeries(metricName, data.Labels{"host": "b"}, timeSlice, valuesB) 39 | require.NoError(t, err) 40 | 41 | var r timeseries.CollectionReader = sc 42 | 43 | c, err := r.GetCollection(true) 44 | 45 | require.NoError(t, c.Warning) 46 | require.Nil(t, err) 47 | require.Nil(t, c.RemainderIndices) 48 | require.Equal(t, expectedRefs, c.Refs) 49 | }) 50 | 51 | t.Run("wide frame", func(t *testing.T) { 52 | sc, err := timeseries.NewWideFrame("B", timeseries.WideFrameVersionLatest) 53 | require.NoError(t, err) 54 | 55 | err = sc.SetTime("time", timeSlice) 56 | require.NoError(t, err) 57 | 58 | err = sc.AddSeries(metricName, valuesA, data.Labels{"host": "a"}, nil) 59 | require.NoError(t, err) 60 | 61 | err = sc.AddSeries(metricName, valuesB, data.Labels{"host": "b"}, nil) 62 | require.NoError(t, err) 63 | 64 | var r timeseries.CollectionReader = sc 65 | 66 | c, err := r.GetCollection(true) 67 | require.Nil(t, err) 68 | 69 | require.NoError(t, c.Warning) 70 | require.Nil(t, c.RemainderIndices) 71 | require.Equal(t, expectedRefs, c.Refs) 72 | }) 73 | 74 | t.Run("long frame", func(t *testing.T) { 75 | ls := ×eries.LongFrame{data.NewFrame("", 76 | data.NewField("time", nil, []time.Time{timeSlice[0], timeSlice[0], 77 | timeSlice[1], timeSlice[1]}), 78 | data.NewField("os.cpu", nil, []float64{valuesA[0], valuesB[0], 79 | valuesA[1], valuesB[1]}), 80 | data.NewField("host", nil, []string{"a", "b", "a", "b"}), 81 | ).SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesLong, TypeVersion: data.FrameTypeVersion{0, 1}}), 82 | } 83 | 84 | var r timeseries.CollectionReader = ls 85 | 86 | c, err := r.GetCollection(true) 87 | require.Nil(t, err) 88 | 89 | require.NoError(t, c.Warning) 90 | require.Nil(t, c.RemainderIndices) 91 | require.Equal(t, expectedRefs, c.Refs) 92 | }) 93 | } 94 | 95 | func addFields(frame *data.Frame, fields ...*data.Field) *data.Frame { 96 | frame.Fields = append(frame.Fields, fields...) 97 | return frame 98 | } 99 | 100 | func TestNoDataFromNew(t *testing.T) { 101 | var multi, wide, long timeseries.CollectionReader 102 | var err error 103 | 104 | multi, err = timeseries.NewMultiFrame("A", timeseries.MultiFrameVersionLatest) 105 | require.NoError(t, err) 106 | 107 | wide, err = timeseries.NewWideFrame("B", timeseries.WideFrameVersionLatest) 108 | require.NoError(t, err) 109 | 110 | long, err = timeseries.NewLongFrame("C", timeseries.LongFrameVersionLatest) 111 | require.NoError(t, err) 112 | 113 | noDataReqs := func(c timeseries.Collection, err error) { 114 | require.NoError(t, err) 115 | require.Nil(t, c.RemainderIndices) 116 | require.NotNil(t, c.Refs) 117 | require.Len(t, c.Refs, 0) 118 | require.NoError(t, c.Warning) 119 | require.True(t, c.NoData()) 120 | } 121 | 122 | viaFrames := func(r timeseries.CollectionReader) { 123 | t.Run("should work when losing go type via Frames()", func(t *testing.T) { 124 | frames := r.Frames() 125 | r, err := timeseries.CollectionReaderFromFrames(frames) 126 | require.NoError(t, err) 127 | 128 | c, err := r.GetCollection(true) 129 | noDataReqs(c, err) 130 | }) 131 | } 132 | 133 | t.Run("multi", func(t *testing.T) { 134 | c, err := multi.GetCollection(true) 135 | noDataReqs(c, err) 136 | viaFrames(multi) 137 | }) 138 | 139 | t.Run("wide", func(t *testing.T) { 140 | c, err := wide.GetCollection(true) 141 | noDataReqs(c, err) 142 | viaFrames(wide) 143 | }) 144 | 145 | t.Run("long", func(t *testing.T) { 146 | c, err := long.GetCollection(true) 147 | noDataReqs(c, err) 148 | viaFrames(long) 149 | }) 150 | } 151 | -------------------------------------------------------------------------------- /docusaurus/website/src/css/theme.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --spacing: 8; 3 | --htmlFontSize: 14; 4 | --fontFamily: 'Inter', 'Helvetica', 'Arial', sans-serif; 5 | --fontFamilyMonospace: 'Roboto Mono', monospace; 6 | --fontSize: 14; 7 | --fontWeightLight: 300; 8 | --fontWeightRegular: 400; 9 | --fontWeightMedium: 500; 10 | --fontWeightBold: 500; 11 | --size-base: 14px; 12 | --size-xs: 10px; 13 | --size-sm: 12px; 14 | --size-md: 14px; 15 | --size-lg: 18px; 16 | --h1-fontFamily: 'Inter', 'Helvetica', 'Arial', sans-serif; 17 | --h1-fontWeight: 400; 18 | --h1-fontSize: 2rem; 19 | --h1-lineHeight: 1.1428571428571428; 20 | --h1-letterSpacing: -0.00893em; 21 | --h2-fontFamily: 'Inter', 'Helvetica', 'Arial', sans-serif; 22 | --h2-fontWeight: 400; 23 | --h2-fontSize: 1.7142857142857142rem; 24 | --h2-lineHeight: 1.1666666666666667; 25 | --h2-letterSpacing: 0em; 26 | --h3-fontFamily: 'Inter', 'Helvetica', 'Arial', sans-serif; 27 | --h3-fontWeight: 400; 28 | --h3-fontSize: 1.5714285714285714rem; 29 | --h3-lineHeight: 1.0909090909090908; 30 | --h3-letterSpacing: 0em; 31 | --h4-fontFamily: 'Inter', 'Helvetica', 'Arial', sans-serif; 32 | --h4-fontWeight: 400; 33 | --h4-fontSize: 1.2857142857142858rem; 34 | --h4-lineHeight: 1.2222222222222223; 35 | --h4-letterSpacing: 0.01389em; 36 | --h5-fontFamily: 'Inter', 'Helvetica', 'Arial', sans-serif; 37 | --h5-fontWeight: 400; 38 | --h5-fontSize: 1.1428571428571428rem; 39 | --h5-lineHeight: 1.375; 40 | --h5-letterSpacing: 0em; 41 | --h6-fontFamily: 'Inter', 'Helvetica', 'Arial', sans-serif; 42 | --h6-fontWeight: 500; 43 | --h6-fontSize: 1rem; 44 | --h6-lineHeight: 1.5714285714285714; 45 | --h6-letterSpacing: 0.01071em; 46 | --body-fontFamily: 'Inter', 'Helvetica', 'Arial', sans-serif; 47 | --body-fontWeight: 400; 48 | --body-fontSize: 1rem; 49 | --body-lineHeight: 1.5714285714285714; 50 | --body-letterSpacing: 0.01071em; 51 | --bodySmall-fontFamily: 'Inter', 'Helvetica', 'Arial', sans-serif; 52 | --bodySmall-fontWeight: 400; 53 | --bodySmall-fontSize: 0.8571428571428571rem; 54 | --bodySmall-lineHeight: 1.5; 55 | --bodySmall-letterSpacing: 0.0125em; 56 | --mode: dark; 57 | --whiteBase: 204, 204, 220; 58 | --border-weak: rgba(204, 204, 220, 0.12); 59 | --border-medium: rgba(204, 204, 220, 0.2); 60 | --border-strong: rgba(204, 204, 220, 0.3); 61 | --text-primary: rgb(204, 204, 220); 62 | --text-secondary: rgba(204, 204, 220, 0.65); 63 | --text-disabled: rgba(204, 204, 220, 0.6); 64 | --text-link: #6e9fff; 65 | --text-maxContrast: #ffffff; 66 | --primary-main: #3d71d9; 67 | --primary-text: #6e9fff; 68 | --primary-border: #6e9fff; 69 | --primary-name: primary; 70 | --primary-shade: rgb(90, 134, 222); 71 | --primary-transparent: #3d71d926; 72 | --primary-contrastText: #ffffff; 73 | --secondary-main: rgba(204, 204, 220, 0.1); 74 | --secondary-shade: rgba(204, 204, 220, 0.14); 75 | --secondary-transparent: rgba(204, 204, 220, 0.08); 76 | --secondary-text: rgb(204, 204, 220); 77 | --secondary-contrastText: rgb(204, 204, 220); 78 | --secondary-border: rgba(204, 204, 220, 0.08); 79 | --secondary-name: secondary; 80 | --info-main: #3d71d9; 81 | --info-text: #6e9fff; 82 | --info-border: #6e9fff; 83 | --info-name: info; 84 | --info-shade: rgb(90, 134, 222); 85 | --info-transparent: #3d71d926; 86 | --info-contrastText: #ffffff; 87 | --error-main: #d10e5c; 88 | --error-text: #ff5286; 89 | --error-name: error; 90 | --error-border: #ff5286; 91 | --error-shade: rgb(215, 50, 116); 92 | --error-transparent: #d10e5c26; 93 | --error-contrastText: #ffffff; 94 | --success-main: #1a7f4b; 95 | --success-text: #6ccf8e; 96 | --success-name: success; 97 | --success-border: #6ccf8e; 98 | --success-shade: rgb(60, 146, 102); 99 | --success-transparent: #1a7f4b26; 100 | --success-contrastText: #ffffff; 101 | --warning-main: #ff9900; 102 | --warning-text: #fbad37; 103 | --warning-name: warning; 104 | --warning-border: #fbad37; 105 | --warning-shade: rgb(255, 168, 38); 106 | --warning-transparent: #ff990026; 107 | --warning-contrastText: #000000; 108 | --background-canvas: #111217; 109 | --background-primary: #181b1f; 110 | --background-secondary: #22252b; 111 | --action-hover: rgba(204, 204, 220, 0.16); 112 | --action-selected: rgba(204, 204, 220, 0.12); 113 | --action-focus: rgba(204, 204, 220, 0.16); 114 | --action-hoverOpacity: 0.08; 115 | --action-disabledText: rgba(204, 204, 220, 0.6); 116 | --action-disabledBackground: rgba(204, 204, 220, 0.04); 117 | --action-disabledOpacity: 0.38; 118 | --gradients-brandHorizontal: linear-gradient(270deg, #f55f3e 0%, #ff8833 100%); 119 | --gradients-brandVertical: linear-gradient(0.01deg, #f55f3e 0.01%, #ff8833 99.99%); 120 | --contrastThreshold: 3; 121 | --hoverFactor: 0.03; 122 | --tonalOffset: 0.15; 123 | } 124 | -------------------------------------------------------------------------------- /docusaurus/website/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const { grafanaPrismTheme } = require("./src/theme/prism"); 5 | 6 | /** @type {import('@docusaurus/types').Config} */ 7 | const config = { 8 | title: "Grafana Data Structure", 9 | tagline: "A contract of data types as the source of truth", 10 | url: "https://grafana.github.io/", 11 | baseUrl: "dataplane/", 12 | onBrokenLinks: "throw", 13 | onBrokenMarkdownLinks: "warn", 14 | favicon: "img/favicon.png", 15 | organizationName: "grafana", 16 | projectName: "dataplane", 17 | i18n: { 18 | defaultLocale: "en", 19 | locales: ["en"], 20 | }, 21 | plugins: [], 22 | presets: [ 23 | [ 24 | "classic", 25 | /** @type {import('@docusaurus/preset-classic').Options} */ 26 | ({ 27 | docs: { 28 | routeBasePath: "/", 29 | path: "../../docs/contract", 30 | sidebarPath: require.resolve("./sidebars.js"), 31 | // Please change this to your repo. 32 | // Remove this to remove the "edit this page" links. 33 | editUrl: 34 | "https://github.com/grafana/dataplane/edit/main/docusaurus/website", 35 | }, 36 | theme: { 37 | customCss: require.resolve("./src/css/custom.css"), 38 | }, 39 | }), 40 | ], 41 | ], 42 | customFields: { 43 | rudderStackTracking: { 44 | url: 'https://rs.grafana.com', 45 | writeKey: '1sBAgwTlZ2K0zTzkM8YTWorZI00', 46 | configUrl: 'https://rsc.grafana.com', 47 | sdkUrl: 'https://rsdk.grafana.com', 48 | }, 49 | canSpamUrl: 'https://grafana.com/canspam', 50 | gcomUrl: 'https://grafana.com/api', 51 | oneTrust: { 52 | enabled: true, 53 | scriptSrc: 'https://cdn.cookielaw.org/scripttemplates/otSDKStub.js', 54 | domainId: '019644f3-5dcf-741c-8b6d-42fb8feae57f', 55 | analyticsGroupId: 'C0002', // OneTrust group ID for analytics consent 56 | }, 57 | }, 58 | themeConfig: 59 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 60 | ({ 61 | navbar: { 62 | title: "Grafana Data Plane", 63 | logo: { 64 | alt: "Grafana Logo", 65 | src: "img/logo.svg", 66 | }, 67 | items: [ 68 | { 69 | type: "doc", 70 | docId: "contract", 71 | position: "right", 72 | label: "Contract", 73 | }, 74 | { 75 | href: "https://www.github.com/grafana/dataplane", 76 | label: "GitHub", 77 | position: "right", 78 | }, 79 | ], 80 | }, 81 | footer: { 82 | style: "dark", 83 | links: [ 84 | { 85 | title: "Docs", 86 | items: [ 87 | { 88 | label: "Contract", 89 | to: "/contract", 90 | }, 91 | ], 92 | }, 93 | { 94 | title: "Tools & Examples", 95 | items: [ 96 | { 97 | label: "Mock Data Source Plugin", 98 | href: "https://grafana.com/plugins/grafana-mock-datasource", 99 | }, 100 | { 101 | label: "Example Data Frames (JSON)", 102 | href: "https://github.com/grafana/dataplane/tree/main/examples/data", 103 | }, 104 | { 105 | label: "Go Testing/Example Library", 106 | href: "https://pkg.go.dev/github.com/grafana/dataplane/examples", 107 | }, 108 | { 109 | label: "Go Dataplane Library", 110 | href: "https://pkg.go.dev/github.com/grafana/dataplane/sdata", 111 | }, 112 | ], 113 | }, 114 | { 115 | title: "Other Resources", 116 | items: [ 117 | { 118 | label: "Go Plugin Data Package", 119 | href: "hhttps://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/data", 120 | }, 121 | ], 122 | }, 123 | { 124 | title: "Community", 125 | items: [ 126 | { 127 | label: "GitHub", 128 | href: "https://www.github.com/grafana/dataplane", 129 | }, 130 | { 131 | label: "Github Issues", 132 | href: "https://www.github.com/grafana/dataplane/issues", 133 | }, 134 | ], 135 | }, 136 | ], 137 | copyright: `Copyright © ${new Date().getFullYear()} Grafana Labs. Built with Docusaurus.`, 138 | }, 139 | prism: { 140 | theme: grafanaPrismTheme, 141 | }, 142 | colorMode: { 143 | defaultMode: "dark", 144 | disableSwitch: true, 145 | respectPrefersColorScheme: false, 146 | }, 147 | }), 148 | }; 149 | 150 | module.exports = config; 151 | -------------------------------------------------------------------------------- /sdata/timeseries/series.go: -------------------------------------------------------------------------------- 1 | package timeseries 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "time" 7 | 8 | "github.com/grafana/dataplane/sdata" 9 | "github.com/grafana/grafana-plugin-sdk-go/data" 10 | ) 11 | 12 | type CollectionReader interface { 13 | // GetCollection runs validate without validateData. If the data is valid, then 14 | // []TimeSeriesMetricRef is returned from reading as well as any ignored data. If invalid, 15 | // then an error is returned, and no refs or ignoredFieldIndices are returned. 16 | GetCollection(validateData bool) (Collection, error) 17 | 18 | Frames() data.Frames // returns underlying frames 19 | } 20 | 21 | // MetricRef is for reading and contains the data for an individual 22 | // time series. In the cases of the Multi and Wide formats, the Fields are pointers 23 | // to the data in the original frame. In the case of Long new fields are constructed. 24 | type MetricRef struct { 25 | TimeField *data.Field 26 | ValueField *data.Field 27 | // TODO: RefID string 28 | // TODO: Pointer to frame meta? 29 | } 30 | 31 | type Collection struct { 32 | RefID string 33 | Refs []MetricRef 34 | RemainderIndices []sdata.FrameFieldIndex 35 | Warning error 36 | } 37 | 38 | func (c Collection) NoData() bool { 39 | return c.Refs != nil && len(c.Refs) == 0 40 | } 41 | 42 | func CollectionReaderFromFrames(frames []*data.Frame) (CollectionReader, error) { 43 | if len(frames) == 0 { 44 | return nil, fmt.Errorf("must be at least one frame") 45 | } 46 | 47 | firstFrame := frames[0] 48 | if firstFrame == nil { 49 | return nil, fmt.Errorf("nil frames are invalid") 50 | } 51 | if firstFrame.Meta == nil { 52 | return nil, fmt.Errorf("metadata missing from first frame, can not determine type") 53 | } 54 | 55 | mt := firstFrame.Meta.Type 56 | var tcr CollectionReader 57 | 58 | switch { 59 | case mt == data.FrameTypeTimeSeriesMulti: 60 | mfs := MultiFrame(frames) 61 | tcr = &mfs 62 | case mt == data.FrameTypeTimeSeriesLong: 63 | ls := LongFrame(frames) 64 | tcr = &ls // TODO change to Frames for extra/ignored data? 65 | case mt == data.FrameTypeTimeSeriesWide: 66 | wfs := WideFrame(frames) 67 | tcr = &wfs 68 | default: 69 | return nil, fmt.Errorf("unsupported time series type %q", mt) 70 | } 71 | return tcr, nil 72 | } 73 | 74 | func (m MetricRef) GetMetricName() string { 75 | if m.ValueField != nil { 76 | return m.ValueField.Name 77 | } 78 | return "" 79 | } 80 | 81 | // TODO GetFQMetric (or something, Names + Labels) 82 | 83 | func (m MetricRef) GetLabels() data.Labels { 84 | if m.ValueField != nil { 85 | return m.ValueField.Labels 86 | } 87 | return nil 88 | } 89 | 90 | // NullableFloat64Point returns the time and *float64 value at the specified index. 91 | // It will error if the index is out of bounds, or if the value can not be converted 92 | // to a *float64. 93 | func (m MetricRef) NullableFloat64Point(pointIdx int) (time.Time, *float64, error) { 94 | f, err := m.NullableFloat64Value(pointIdx) 95 | if err != nil { 96 | return time.Time{}, nil, err 97 | } 98 | t, err := m.Time(pointIdx) 99 | if err != nil { 100 | return time.Time{}, nil, err 101 | } 102 | return t, f, nil 103 | } 104 | 105 | func (m MetricRef) NullableFloat64Value(pointIdx int) (*float64, error) { 106 | if m.ValueField.Len() < pointIdx { 107 | return nil, fmt.Errorf("pointIdx %v is out of bounds for series", pointIdx) 108 | } 109 | f, err := m.ValueField.NullableFloatAt(pointIdx) 110 | if err != nil { 111 | return nil, err 112 | } 113 | return f, nil 114 | } 115 | 116 | func (m MetricRef) Time(pointIdx int) (time.Time, error) { 117 | if m.TimeField.Len() < pointIdx { 118 | return time.Time{}, fmt.Errorf("pointIdx %v is out of bounds for series", pointIdx) 119 | } 120 | ti := m.TimeField.At(pointIdx) 121 | t, ok := ti.(time.Time) 122 | if !ok { 123 | return time.Time{}, fmt.Errorf("series field is not of expected type time.Time, got %T", ti) 124 | } 125 | return t, nil 126 | } 127 | 128 | func (m MetricRef) Len() (int, error) { 129 | if m.ValueField.Len() != m.TimeField.Len() { 130 | return 0, fmt.Errorf("series has mismatched value and time field lengths") 131 | } 132 | return m.ValueField.Len(), nil 133 | } 134 | 135 | func sortTimeSeriesMetricRef(refs []MetricRef) { 136 | sort.SliceStable(refs, func(i, j int) bool { 137 | iRef := refs[i] 138 | jRef := refs[j] 139 | 140 | if iRef.GetMetricName() < jRef.GetMetricName() { 141 | return true 142 | } 143 | if iRef.GetMetricName() > jRef.GetMetricName() { 144 | return false 145 | } 146 | 147 | // If here Names are equal, next sort based on if there are labels. 148 | if iRef.GetLabels() == nil && jRef.GetLabels() == nil { 149 | return true // no labels first 150 | } 151 | if iRef.GetLabels() == nil && jRef.GetLabels() != nil { 152 | return true 153 | } 154 | if iRef.GetLabels() != nil && jRef.GetLabels() == nil { 155 | return false 156 | } 157 | 158 | return iRef.GetLabels().String() < jRef.GetLabels().String() 159 | }) 160 | } 161 | -------------------------------------------------------------------------------- /docusaurus/website/docusaurus.config.devportal.prod.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const { grafanaPrismTheme } = require("./src/theme/prism"); 5 | 6 | const devPortalHome = "https://grafana.com/developers"; 7 | 8 | /** @type {import('@docusaurus/types').Config} */ 9 | const config = { 10 | title: "Grafana Data Structure", 11 | tagline: "A contract of data types as the source of truth", 12 | url: "https://grafana.com/", 13 | baseUrl: "developers/dataplane/", 14 | onBrokenLinks: "throw", 15 | onBrokenMarkdownLinks: "warn", 16 | favicon: "img/favicon.png", 17 | organizationName: "grafana", 18 | projectName: "dataplane", 19 | i18n: { 20 | defaultLocale: "en", 21 | locales: ["en"], 22 | }, 23 | plugins: [ 24 | [ 25 | "docusaurus-lunr-search", 26 | { 27 | disableVersioning: true, 28 | }, 29 | ], 30 | ], 31 | presets: [ 32 | [ 33 | "classic", 34 | /** @type {import('@docusaurus/preset-classic').Options} */ 35 | ({ 36 | docs: { 37 | routeBasePath: "/", 38 | path: "../../docs/contract", 39 | sidebarPath: require.resolve("./sidebars.js"), 40 | // Please change this to your repo. 41 | // Remove this to remove the "edit this page" links. 42 | editUrl: 43 | "https://github.com/grafana/dataplane/edit/main/docusaurus/website", 44 | }, 45 | theme: { 46 | customCss: require.resolve("./src/css/custom.css"), 47 | }, 48 | blog: false, 49 | }), 50 | ], 51 | ], 52 | customFields: { 53 | rudderStackTracking: { 54 | url: 'https://rs.grafana.com', 55 | writeKey: '1sBAgwTlZ2K0zTzkM8YTWorZI00', 56 | configUrl: 'https://rsc.grafana.com', 57 | sdkUrl: 'https://rsdk.grafana.com', 58 | }, 59 | canSpamUrl: 'https://grafana.com/canspam', 60 | gcomUrl: 'https://grafana.com/api', 61 | oneTrust: { 62 | enabled: true, 63 | scriptSrc: 'https://cdn.cookielaw.org/scripttemplates/otSDKStub.js', 64 | domainId: '019644f3-5dcf-741c-8b6d-42fb8feae57f-devprod', 65 | analyticsGroupId: 'C0002', // OneTrust group ID for analytics consent 66 | }, 67 | }, 68 | themeConfig: 69 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 70 | ({ 71 | navbar: { 72 | title: "Grafana Data Structure", 73 | logo: { 74 | alt: "Grafana Logo", 75 | src: "img/logo.svg", 76 | }, 77 | items: [ 78 | { 79 | href: devPortalHome, 80 | label: "Portal Home", 81 | position: "right", 82 | target: "_self", 83 | }, 84 | { 85 | href: "https://www.github.com/grafana/dataplane", 86 | label: "GitHub", 87 | position: "right", 88 | }, 89 | ], 90 | }, 91 | footer: { 92 | style: "dark", 93 | links: [ 94 | { 95 | title: "Docs", 96 | items: [ 97 | { 98 | label: "Contract", 99 | to: "/", 100 | }, 101 | { 102 | label: "Portal Home", 103 | href: devPortalHome, 104 | target: "_self", 105 | }, 106 | ], 107 | }, 108 | { 109 | title: "Tools & Examples", 110 | items: [ 111 | { 112 | label: "Mock Data Source Plugin", 113 | href: "https://grafana.com/plugins/grafana-mock-datasource", 114 | }, 115 | { 116 | label: "Example Data Frames (JSON)", 117 | href: "https://github.com/grafana/dataplane/tree/main/examples/data", 118 | }, 119 | { 120 | label: "Go Testing/Example Library", 121 | href: "https://pkg.go.dev/github.com/grafana/dataplane/examples", 122 | }, 123 | { 124 | label: "Go Dataplane Library", 125 | href: "https://pkg.go.dev/github.com/grafana/dataplane/sdata", 126 | }, 127 | ], 128 | }, 129 | { 130 | title: "Other Resources", 131 | items: [ 132 | { 133 | label: "Go Plugin Data Package", 134 | href: "hhttps://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/data", 135 | }, 136 | ], 137 | }, 138 | { 139 | title: "Community", 140 | items: [ 141 | { 142 | label: "GitHub", 143 | href: "https://www.github.com/grafana/dataplane", 144 | }, 145 | { 146 | label: "Github Issues", 147 | href: "https://www.github.com/grafana/dataplane/issues", 148 | }, 149 | ], 150 | }, 151 | ], 152 | copyright: `Copyright © ${new Date().getFullYear()} Grafana Labs. Built with Docusaurus.`, 153 | }, 154 | prism: { 155 | theme: grafanaPrismTheme, 156 | }, 157 | colorMode: { 158 | defaultMode: "dark", 159 | disableSwitch: true, 160 | respectPrefersColorScheme: false, 161 | }, 162 | }), 163 | }; 164 | 165 | module.exports = config; 166 | -------------------------------------------------------------------------------- /docusaurus/website/docusaurus.config.devportal.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const { grafanaPrismTheme } = require("./src/theme/prism"); 5 | 6 | const devPortalHome = "https://grafana-dev.com/developers"; 7 | 8 | /** @type {import('@docusaurus/types').Config} */ 9 | const config = { 10 | title: "Grafana Data Structure", 11 | tagline: "A contract of data types as the source of truth", 12 | url: "https://grafana-dev.com/", 13 | baseUrl: "developers/dataplane/", 14 | onBrokenLinks: "throw", 15 | onBrokenMarkdownLinks: "warn", 16 | favicon: "img/favicon.png", 17 | organizationName: "grafana", 18 | projectName: "dataplane", 19 | i18n: { 20 | defaultLocale: "en", 21 | locales: ["en"], 22 | }, 23 | 24 | plugins: [ 25 | [ 26 | "docusaurus-lunr-search", 27 | { 28 | disableVersioning: true, 29 | }, 30 | ], 31 | ], 32 | 33 | presets: [ 34 | [ 35 | "classic", 36 | /** @type {import('@docusaurus/preset-classic').Options} */ 37 | ({ 38 | docs: { 39 | routeBasePath: "/", 40 | path: "../../docs/contract", 41 | sidebarPath: require.resolve("./sidebars.js"), 42 | // Please change this to your repo. 43 | // Remove this to remove the "edit this page" links. 44 | editUrl: 45 | "https://github.com/grafana/dataplane/edit/main/docusaurus/website", 46 | }, 47 | theme: { 48 | customCss: require.resolve("./src/css/custom.css"), 49 | }, 50 | blog: false, 51 | }), 52 | ], 53 | ], 54 | customFields: { 55 | rudderStackTracking: { 56 | url: 'https://rs.grafana.com', 57 | writeKey: '1sBAgwTlZ2K0zTzkM8YTWorZI00', 58 | configUrl: 'https://rsc.grafana.com', 59 | sdkUrl: 'https://rsdk.grafana.com', 60 | }, 61 | canSpamUrl: 'https://grafana.com/canspam', 62 | gcomUrl: 'https://grafana.com/api', 63 | oneTrust: { 64 | enabled: true, 65 | scriptSrc: 'https://cdn.cookielaw.org/scripttemplates/otSDKStub.js', 66 | domainId: '019644f3-5dcf-741c-8b6d-42fb8feae57f-dev', 67 | analyticsGroupId: 'C0002', // OneTrust group ID for analytics consent 68 | }, 69 | }, 70 | themeConfig: 71 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 72 | ({ 73 | navbar: { 74 | title: "Grafana Data Plane", 75 | logo: { 76 | alt: "Grafana Logo", 77 | src: "img/logo.svg", 78 | }, 79 | items: [ 80 | { 81 | href: devPortalHome, 82 | label: "Portal Home", 83 | position: "right", 84 | target: "_self", 85 | }, 86 | { 87 | href: "https://www.github.com/grafana/dataplane", 88 | label: "GitHub", 89 | position: "right", 90 | }, 91 | ], 92 | }, 93 | footer: { 94 | style: "dark", 95 | links: [ 96 | { 97 | title: "Docs", 98 | items: [ 99 | { 100 | label: "Contract", 101 | to: "/", 102 | }, 103 | { 104 | label: "Portal Home", 105 | href: devPortalHome, 106 | target: "_self", 107 | }, 108 | ], 109 | }, 110 | { 111 | title: "Tools & Examples", 112 | items: [ 113 | { 114 | label: "Mock Data Source Plugin", 115 | href: "https://grafana.com/plugins/grafana-mock-datasource", 116 | }, 117 | { 118 | label: "Example Data Frames (JSON)", 119 | href: "https://github.com/grafana/dataplane/tree/main/examples/data", 120 | }, 121 | { 122 | label: "Go Testing/Example Library", 123 | href: "https://pkg.go.dev/github.com/grafana/dataplane/examples", 124 | }, 125 | { 126 | label: "Go Dataplane Library", 127 | href: "https://pkg.go.dev/github.com/grafana/dataplane/sdata", 128 | }, 129 | ], 130 | }, 131 | { 132 | title: "Other Resources", 133 | items: [ 134 | { 135 | label: "Go Plugin Data Package", 136 | href: "hhttps://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/data", 137 | }, 138 | ], 139 | }, 140 | { 141 | title: "Community", 142 | items: [ 143 | { 144 | label: "GitHub", 145 | href: "https://www.github.com/grafana/dataplane", 146 | }, 147 | { 148 | label: "Github Issues", 149 | href: "https://www.github.com/grafana/dataplane/issues", 150 | }, 151 | ], 152 | }, 153 | ], 154 | copyright: `Copyright © ${new Date().getFullYear()} Grafana Labs. Built with Docusaurus.`, 155 | }, 156 | prism: { 157 | theme: grafanaPrismTheme, 158 | }, 159 | colorMode: { 160 | defaultMode: "dark", 161 | disableSwitch: true, 162 | respectPrefersColorScheme: false, 163 | }, 164 | }), 165 | }; 166 | 167 | module.exports = config; 168 | -------------------------------------------------------------------------------- /docs/contract/contract.md: -------------------------------------------------------------------------------- 1 | # Introduction to the Grafana data structure 2 | 3 | Grafana supports a variety of different data sources, each with its own data model. To make this possible, Grafana consolidates the query results from each of these data sources into one unified data structure called a **data frame**. The **data plane** adds a property layer to the data frame with information about the data frame type and what the data frame holds. 4 | 5 | :::tip 6 | 7 | Data plane types are to data frames what TypeScript is to JavaScript. 8 | 9 | ::: 10 | 11 | The data plane contract is a written set of rules that explain how producers of data (data sources, transformations) must form the frames, and how data consumers (like dashboards, alerting, and apps) can expect the data they receive to be like. In short, it describes the rules for valid and invalid schemas for each data frame type. 12 | 13 | ## Data frames overview 14 | 15 | A data frame is a data structure that consolidates the query results from your data sources, providing a common container in Grafana. 16 | 17 | :::caution 18 | 19 | Query responses are often more than one single data frame. 20 | 21 | ::: 22 | 23 | The data frame is a column-oriented table (the _fields_) with metadata (the _frame_) attached. Since data frame columns can have labels attached to them (`key=value`, `key2=val`...), it can hold Prometheus like responses as well. 24 | 25 | Each field in a data frame contains optional information about the values in the field, such as units, scaling, and so on. By adding field configurations to a data frame, Grafana can configure visualizations automatically. For example, you could configure Grafana to automatically set the unit provided by the data source. 26 | 27 | ## Data plane overview 28 | 29 | The data plane adds a property layer to the frame as metadata. It indicates the data frame _type_ (for example, a timeseries or a heatmap), which consists of a _kind_ (of data) and its _format_ (Prometheus-like, SQL-table-like). 30 | 31 | ![Data plane diagram](./images/data-types.jpg) 32 | 33 | The use of data plane is generally not enforced, although it's mandatory for labeled data when using SQL expressions. Refer to [Requirements to support SQL expressions](https://grafana.com/developers/plugin-tools/how-to-guides/data-source-plugins/sql-requirements) to learn more. 34 | 35 | ## Why use the data plane layer? 36 | 37 | The main objective of the data plane is to make Grafana more self-interoperable between data sources and features like dashboards and alerting. With data planes compatibility becomes about supporting data types and not specific features and data sources. 38 | 39 | For example, if data source produces type "A", and alerting and certain visualizations accept type "A", then that data source works with alerting and those visualizations. 40 | 41 | ### Benefits 42 | 43 | Besides interoperability, using data planes has other benefits. 44 | 45 | If you're a developer and data source author, you know what type of frames to output, and authors of features know what to expect for their input. This makes the platform scalable and development more efficient and less frustrating due to incompatibilities. 46 | 47 | In general, using the data plane makes Grafana more reliable, with everything working as expected. A solid data plane contract would help to suggest what to do with your data. For example, if you're using a specific type, Grafana could suggest creating alert rules or certain visualizations in dashboards that work well with that type. Similarly, Grafana could suggest transformations that get you from the current type to another type support additional actions. 48 | 49 | ## What if I don't use the data plane layer? 50 | 51 | If you don't use a data plane, consumers of data have to infer the type from the data returned, which has a few problems: 52 | 53 | - Users are uncertain about how to write queries to work with different things. 54 | - Error messages can become seemingly unrelated to what users are doing. 55 | - Different features guess differently (for example, alerting vs. visualizations), making it hard for users and developers to know what to send. 56 | - On the consumer side, guessing code becomes more convoluted over time as more exceptions are added for various data sources. 57 | 58 | ### What if my data source is schemaless and doesn't have kinds or types? 59 | 60 | You can propose a new data plane type: They're designed to grow into maturity, not limit innovation. 61 | 62 | Usually data sources have a drop down in the query UI to assert the query type, which appears as "format as". You can use this data source query information to produce a data plane-compatible type for the response. 63 | 64 | While this may involve extra work for the user, defining the data plane type is easier at query time, since the data source knows more about the data that comes from the system behind the data source. 65 | 66 | ## List of data sources that use the data plane 67 | 68 | As of October 2025, the following data sources send data plane data in at least some of their responses: 69 | 70 | - Prometheus, including Amazon and Azure variants 71 | - Loki 72 | - Azure Monitor 73 | - Azure Data Explorer 74 | - Bigquery 75 | - Clickhouse 76 | - Cloudlflare 77 | - Databricks 78 | - Influx 79 | - MySQL 80 | - New Relic 81 | - Oracle 82 | - Postgres 83 | - Snowflake 84 | - Victoria metrics 85 | 86 | To see examples of data planes, refer to [data plane example data](https://github.com/grafana/dataplane/tree/main/examples/data) in GitHub. 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /sdata/timeseries/long.go: -------------------------------------------------------------------------------- 1 | package timeseries 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/grafana/dataplane/sdata" 8 | "github.com/grafana/grafana-plugin-sdk-go/data" 9 | ) 10 | 11 | // LongFrame is a time series format where all series live in one frame. 12 | // This time series format should be used with Table-like sources (e.g. SQL) that 13 | // do not have a native concept of Labels. 14 | type LongFrame []*data.Frame 15 | 16 | var LongFrameVersionLatest = LongFrameVersions()[len(LongFrameVersions())-1] 17 | 18 | func LongFrameVersions() []data.FrameTypeVersion { 19 | return []data.FrameTypeVersion{{0, 1}} 20 | } 21 | 22 | func NewLongFrame(refID string, v data.FrameTypeVersion) (*LongFrame, error) { 23 | if v.Greater(LongFrameVersionLatest) { 24 | return nil, fmt.Errorf("can not create LongFrame of version %s because it is newer than library version %v", v, LongFrameVersionLatest) 25 | } 26 | return &LongFrame{emptyFrameWithTypeMD(refID, data.FrameTypeTimeSeriesLong, v)}, nil 27 | } 28 | 29 | func (ls *LongFrame) GetCollection(validateData bool) (Collection, error) { 30 | return validateAndGetRefsLong(ls, validateData, true) 31 | } 32 | 33 | func validateAndGetRefsLong(ls *LongFrame, validateData, getRefs bool) (Collection, error) { 34 | var c Collection 35 | switch { 36 | case ls == nil: 37 | return c, fmt.Errorf("frames may not be nil") 38 | case len(*ls) == 0: 39 | return c, fmt.Errorf("missing frame, must be at least one frame") 40 | } 41 | 42 | frame := (*ls)[0] 43 | 44 | if frame == nil { 45 | return c, fmt.Errorf("frame 0 must not be nil") 46 | } 47 | 48 | c.RefID = frame.RefID 49 | 50 | if !frameHasType(frame, data.FrameTypeTimeSeriesLong) { 51 | return c, fmt.Errorf("frame 0 is missing long type indicator") 52 | } 53 | 54 | if frame.Meta.TypeVersion != LongFrameVersionLatest { 55 | c.Warning = &sdata.VersionWarning{DataVersion: frame.Meta.TypeVersion, LibraryVersion: LongFrameVersionLatest, DataType: data.FrameTypeTimeSeriesLong} 56 | } 57 | 58 | if len(frame.Fields) == 0 { // empty response 59 | if err := ignoreAdditionalFrames("additional frame on empty response", *ls, &c.RemainderIndices); err != nil { 60 | return c, err 61 | } 62 | // Empty Response 63 | c.Refs = []MetricRef{} 64 | return c, nil 65 | } 66 | 67 | if err := malformedFrameCheck(0, frame); err != nil { 68 | return c, err 69 | } 70 | 71 | // metricName/labels -> SeriesRef 72 | mm := make(map[string]map[string]MetricRef) 73 | 74 | timeField, remainderTimeFields, err := seriesCheckSelectTime(0, frame) 75 | if err != nil { 76 | return c, err 77 | } 78 | if remainderTimeFields != nil { 79 | c.RemainderIndices = append(c.RemainderIndices, remainderTimeFields...) 80 | } 81 | 82 | valueFieldIndices := frame.TypeIndices(sdata.ValidValueFields()...) // TODO switch on bool type option 83 | if len(valueFieldIndices) == 0 { 84 | return c, fmt.Errorf("frame is missing a numeric value field") 85 | } 86 | 87 | factorFieldIndices := frame.TypeIndices(data.FieldTypeString, data.FieldTypeNullableString) 88 | 89 | appendToMetric := func(metricName string, l data.Labels, t time.Time, value interface{}, valType data.FieldType) error { 90 | if mm[metricName] == nil { 91 | mm[metricName] = make(map[string]MetricRef) 92 | } 93 | 94 | lbStr := l.String() 95 | if ref, ok := mm[metricName][lbStr]; !ok { 96 | ref.TimeField = data.NewField(timeField.Name, nil, []time.Time{t}) 97 | 98 | ref.ValueField = data.NewFieldFromFieldType(valType, 1) 99 | ref.ValueField.Set(0, value) 100 | ref.ValueField.Name = metricName 101 | ref.ValueField.Labels = l 102 | 103 | mm[metricName][lbStr] = ref 104 | c.Refs = append(c.Refs, ref) 105 | } else { 106 | if validateData && ref.TimeField.Len() > 1 { 107 | prevTime := ref.TimeField.At(ref.TimeField.Len() - 1).(time.Time) 108 | if prevTime.After(t) { 109 | return fmt.Errorf("unsorted time field") 110 | } 111 | if prevTime.Equal(t) { 112 | return fmt.Errorf("duplicate data points in metric %v %v", metricName, lbStr) 113 | } 114 | } 115 | ref.TimeField.Append(t) 116 | ref.ValueField.Append(value) 117 | } 118 | return nil 119 | } 120 | 121 | if getRefs { 122 | for rowIdx := 0; rowIdx < frame.Rows(); rowIdx++ { 123 | l := data.Labels{} 124 | for _, strFieldIdx := range factorFieldIndices { 125 | cv, _ := frame.ConcreteAt(strFieldIdx, rowIdx) 126 | l[frame.Fields[strFieldIdx].Name] = cv.(string) 127 | } 128 | for _, vFieldIdx := range valueFieldIndices { 129 | valueField := frame.Fields[vFieldIdx] 130 | if err := appendToMetric(valueField.Name, l, timeField.At(rowIdx).(time.Time), valueField.At(rowIdx), valueField.Type()); err != nil { 131 | return c, err 132 | } 133 | } 134 | } 135 | sortTimeSeriesMetricRef(c.Refs) 136 | } 137 | 138 | // TODO this is fragile if new types are added 139 | otherFields := frame.TypeIndices(data.FieldTypeNullableTime) 140 | for _, fieldIdx := range otherFields { 141 | c.RemainderIndices = append(c.RemainderIndices, sdata.FrameFieldIndex{ 142 | FrameIdx: 0, FieldIdx: fieldIdx, 143 | Reason: fmt.Sprintf("unsupported field type %v", frame.Fields[fieldIdx].Type())}, 144 | ) 145 | } 146 | 147 | if err := ignoreAdditionalFrames("additional frame", *ls, &c.RemainderIndices); err != nil { 148 | return c, err 149 | } 150 | 151 | return c, nil 152 | } 153 | 154 | func (ls *LongFrame) Frames() data.Frames { 155 | if ls == nil { 156 | return nil 157 | } 158 | return data.Frames(*ls) 159 | } 160 | -------------------------------------------------------------------------------- /sdata/numeric/numeric_test.go: -------------------------------------------------------------------------------- 1 | package numeric_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/grafana/dataplane/sdata/numeric" 7 | "github.com/grafana/grafana-plugin-sdk-go/data" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestSimpleNumeric(t *testing.T) { 12 | // addMetrics uses the writer interface to add sample metrics 13 | addMetrics := func(c numeric.CollectionWriter) { 14 | err := c.AddMetric("os.cpu", data.Labels{"host": "a"}, 1.0) 15 | require.NoError(t, err) 16 | err = c.AddMetric("os.cpu", data.Labels{"host": "b"}, 2.0) 17 | require.NoError(t, err) 18 | } 19 | 20 | // refs should be same across the formats 21 | expectedRefs := []numeric.MetricRef{ 22 | { 23 | ValueField: data.NewField("os.cpu", data.Labels{"host": "a"}, []float64{1}), 24 | }, 25 | { 26 | ValueField: data.NewField("os.cpu", data.Labels{"host": "b"}, []float64{2}), 27 | }, 28 | } 29 | 30 | t.Run("multi frame", func(t *testing.T) { 31 | var mFrameNC numeric.CollectionRW 32 | var err error 33 | mFrameNC, err = numeric.NewMultiFrame("A", numeric.MultiFrameVersionLatest) 34 | require.NoError(t, err) 35 | 36 | addMetrics(mFrameNC) 37 | 38 | mc, err := mFrameNC.GetCollection(false) 39 | require.NoError(t, mc.Warning) 40 | require.Nil(t, err) 41 | require.Nil(t, mc.RemainderIndices) 42 | require.Equal(t, expectedRefs, mc.Refs) 43 | }) 44 | 45 | t.Run("wide frame", func(t *testing.T) { 46 | var wFrameNC numeric.CollectionRW 47 | var err error 48 | wFrameNC, err = numeric.NewWideFrame("B", numeric.WideFrameVersionLatest) 49 | require.NoError(t, err) 50 | 51 | addMetrics(wFrameNC) 52 | 53 | wc, err := wFrameNC.GetCollection(false) 54 | require.NoError(t, wc.Warning) 55 | require.Nil(t, err) 56 | require.Nil(t, wc.RemainderIndices) 57 | require.Equal(t, expectedRefs, wc.Refs) 58 | }) 59 | t.Run("long frame", func(t *testing.T) { 60 | lfn, err := numeric.NewLongFrame("C", numeric.LongFrameVersionLatest) 61 | require.NoError(t, err) 62 | 63 | lfn.Fields = append(lfn.Fields, data.NewField("os.cpu", nil, []float64{1, 2}), 64 | data.NewField("host", nil, []string{"a", "b"})) 65 | var lcr numeric.CollectionReader = lfn 66 | 67 | lc, err := lcr.GetCollection(false) 68 | require.NoError(t, lc.Warning) 69 | require.Nil(t, err) 70 | require.Nil(t, lc.RemainderIndices) 71 | require.Equal(t, expectedRefs, lc.Refs) 72 | }) 73 | } 74 | 75 | func TestNoDataFromNew(t *testing.T) { 76 | var multi, wide, long numeric.CollectionReader 77 | var err error 78 | 79 | multi, err = numeric.NewMultiFrame("A", numeric.MultiFrameVersionLatest) 80 | require.NoError(t, err) 81 | 82 | wide, err = numeric.NewWideFrame("B", numeric.WideFrameVersionLatest) 83 | require.NoError(t, err) 84 | 85 | long, err = numeric.NewLongFrame("C", numeric.LongFrameVersionLatest) 86 | require.NoError(t, err) 87 | 88 | noDataReqs := func(c numeric.Collection, err error) { 89 | require.NoError(t, err) 90 | require.Nil(t, c.RemainderIndices) 91 | require.NotNil(t, c.Refs) 92 | require.Len(t, c.Refs, 0) 93 | require.NoError(t, c.Warning) 94 | require.True(t, c.NoData()) 95 | } 96 | 97 | viaFrames := func(r numeric.CollectionReader) { 98 | t.Run("should work when losing go type via Frames()", func(t *testing.T) { 99 | frames := r.Frames() 100 | r, err := numeric.CollectionReaderFromFrames(frames) 101 | require.NoError(t, err) 102 | 103 | c, err := r.GetCollection(false) 104 | noDataReqs(c, err) 105 | }) 106 | } 107 | 108 | t.Run("multi", func(t *testing.T) { 109 | c, err := multi.GetCollection(false) 110 | noDataReqs(c, err) 111 | viaFrames(multi) 112 | }) 113 | 114 | t.Run("wide", func(t *testing.T) { 115 | c, err := wide.GetCollection(false) 116 | noDataReqs(c, err) 117 | viaFrames(wide) 118 | }) 119 | 120 | t.Run("long", func(t *testing.T) { 121 | c, err := long.GetCollection(false) 122 | noDataReqs(c, err) 123 | viaFrames(long) 124 | }) 125 | } 126 | 127 | func TestSortNumericMetricRef(t *testing.T) { 128 | t.Run("sort by metric name", func(t *testing.T) { 129 | metricA := numeric.MetricRef{ValueField: data.NewField("metric_a", nil, []float64{})} 130 | metricB := numeric.MetricRef{ValueField: data.NewField("metric_b", nil, []float64{})} 131 | metricC := numeric.MetricRef{ValueField: data.NewField("metric_c", nil, []float64{})} 132 | input := []numeric.MetricRef{ 133 | metricC, 134 | metricA, 135 | metricB, 136 | } 137 | numeric.SortNumericMetricRef(input) 138 | expected := []numeric.MetricRef{ 139 | metricA, 140 | metricB, 141 | metricC, 142 | } 143 | 144 | require.Equal(t, expected, input) 145 | }) 146 | 147 | t.Run("sort by labels", func(t *testing.T) { 148 | metricA := numeric.MetricRef{ValueField: data.NewField("metric", data.Labels{"a": "1"}, []float64{})} 149 | metricB := numeric.MetricRef{ValueField: data.NewField("metric", data.Labels{"b": "2"}, []float64{})} 150 | metricC := numeric.MetricRef{ValueField: data.NewField("metric", data.Labels{"c": "3"}, []float64{})} 151 | metricNilLabel := numeric.MetricRef{ValueField: data.NewField("metric", nil, []float64{})} 152 | input := []numeric.MetricRef{ 153 | metricNilLabel, 154 | metricC, 155 | metricA, 156 | metricNilLabel, 157 | metricB, 158 | } 159 | numeric.SortNumericMetricRef(input) 160 | expected := []numeric.MetricRef{ 161 | metricNilLabel, 162 | metricNilLabel, 163 | metricA, 164 | metricB, 165 | metricC, 166 | } 167 | 168 | require.Equal(t, expected, input) 169 | }) 170 | } 171 | -------------------------------------------------------------------------------- /docs/contract/contract-spec.md: -------------------------------------------------------------------------------- 1 | # Data Plane Contract - Technical Specification 2 | 3 | Grafana supports a variety of different data sources, each with its own data model. To make this possible, Grafana consolidates the query results from each of these data sources into one unified data structure called a **data frame**. The **data plane** adds a property layer to the data frame with information about the data frame type. Read [Grafana data structure](./contract.md) for an introduction to data frames and the data plane layer. 4 | 5 | ## How is the data plane layer built? 6 | 7 | The data plane layer indicates the data frame **type** (for example: time series data, numeric, or a histogram). In turn, the data frame type consists of a **kind** of data (time series, numeric...) and the data **format** (wide, long, multi, Prometheus-like, SQL-table-like...). 8 | 9 | For example, the `TimeSeriesWide` type consists of the kind "Time Series" and the format "Wide". 10 | 11 | ## Available data types 12 | 13 | The following data frame types are available: 14 | 15 | - [Time series](./timeseries.md) 16 | - [Wide](./timeseries.md#time-series-wide-format-timeserieswide) 17 | - [Long](./timeseries.md#time-series-long-format-timeserieslong-sql-like) 18 | - [Multi](./timeseries.md#time-series-multi-format-timeseriesmulti) 19 | - [Numeric](./numeric.md) 20 | - [Wide](./numeric.md#numeric-wide-format-numericwide) 21 | - [Multi](./numeric.md#numeric-multi-format-numericmulti) 22 | - [Long](./numeric.md#numeric-many-format-numericlong) 23 | - [Heatmap](./heatmap.md) 24 | - [Rows](./heatmap.md#heatmap-rows-heatmaprows) 25 | - [Cells](./heatmap.md#heatmap-cells-heatmapcells) 26 | - [Logs](./logs.md) 27 | - [LogLines](./logs.md#loglines) 28 | 29 | ## Data sets 30 | 31 | A data type (kind+format) can have multiple items, forming a **set** of data items. For example, the numeric kind can have a set of numbers, or the time series kind can have a set of time series. 32 | 33 | Each item of data in a set is uniquely identified by its **name** and its **dimensions**. Dimensions are facets of data (such as "location" or "host") with a corresponding value. For example, {"host"="a", "location"="new york"}. 34 | 35 | In a data frame, dimensions are in either a field's label property or in string field. 36 | 37 | ### Properties of dimensional set-based kinds 38 | 39 | - If multiple items have the same name, they need to have different dimensions (for example, labels) that uniquely identifies each item. 40 | - The item name should appear in the `name` property of each value (numeric or bool typed) field, as any other label. 41 | - A response can have different item names in the response. Note that Server Side Expressions (SSE) doesn't currently handle this option. 42 | 43 | ## Remainder data 44 | 45 | Data is encoded into data frame(s), therefore all types are implemented as an array of `data.Frame`. 46 | 47 | There can be data in data frame(s) that's not part of the data type's data. This extra data is the **remainder data** and is free to be used as convenient. What data becomes remainder data is dependent on and specified in the data type. Generally, it can be additional frames and/or additional fields of a certain field type. 48 | 49 | :::caution 50 | If you chose to use reminder data, libraries based on this contract must clearly delineate remainder data from data that is part of the type. 51 | ::: 52 | 53 | ## Possible responses 54 | 55 | ### Empty item response 56 | 57 | If you retrieve one or more data items from a data source but an item has no values, that item is said to be an **"Empty value"**. In this case, the required data frame fields need to be present, although the fields themselves will have no values. 58 | 59 | ### "No Data" response 60 | 61 | If a response has no data items, the response is a **"No Data"** response. 62 | 63 | No data response can happen: 64 | 65 | - If the entire set has no items. 66 | - If the response has no frame and no error is returned. 67 | 68 | If you have a response with no data, send a single frame (containing the data type, if applicable) and don't use any other fields on that frame. 69 | 70 | ### Invalid data response 71 | 72 | If a data type is specified but the response doesn't follow the data type's rules, you'll get an error. 73 | 74 | ### Error responses 75 | 76 | If a query returns an error, the error response is returned from outside the data frames using the `Error` and `Status` properties on [DataResponse](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend#DataResponse). When an error is returned with the DataResponse, a single frame with no fields may be included as well, but it won't be considered **"No Data"** due to the error. This frame is included so that metadata, in particular a frame's `ExecutedQueryString`, can be returned with the error. 77 | 78 | In a plugin backend, the call [`DataQueryHandler`](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend#QueryDataHandler) can return an error. Use this option only when the entire request (all queries) fail. 79 | 80 | ### Responses with multiple data types 81 | 82 | :::caution 83 | Multiple data type responeses are not supported at the moment. 84 | ::: 85 | 86 | If you don't use multi-type responses, you'll get the first data type that matches what you're querying for. 87 | 88 | Although not supported, if you need to use responses with multiple data types (within a `RefID`), the following applies: 89 | 90 | - Responses might not work as expected. 91 | - Use only one format per data type within a response. For example, you may use TimeSeriesWide and NumericLong, but do not mix TimeSeriesWide and TimeSeriesLong. 92 | - Derive the borders between the types from adjacent frames (within the array of frames) that share the same data type. 93 | 94 | ## Versioning 95 | 96 | :::important 97 | The data plane contract needs to be as stable as possible. 98 | ::: 99 | 100 | Versioning recommendations: 101 | 102 | - Use contract versions only for changes impacting overarching concepts such as error handling or multi-data type responses. In other words, when version `1.0` is reached, limit changes to enhancements before working on version `2.0`. 103 | - The addition of new data types, or the modification of data types should not impact the contract version. 104 | 105 | ### Data type versions 106 | 107 | Give each data type a version in major/minor form (x.x). The version is located in the `Frame.Meta.TypeVersion` property. 108 | 109 | - Version `0.0` means the data type is either pre contract, or in very early development. 110 | - Version `0.x` means the data type is well defined in the contract, but may change based on things learned for wider usage. 111 | - Version `1.0` should be a stable data type, and should have no changes from the previous version. 112 | - Minor version changes beyond `1.0` must be backward compatible for data reader implementations. They also must be forward compatible with other `1.x` versions for readers as well (but without enhancement support). 113 | 114 | 115 | -------------------------------------------------------------------------------- /sdata/timeseries/wide.go: -------------------------------------------------------------------------------- 1 | package timeseries 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/grafana/dataplane/sdata" 8 | "github.com/grafana/grafana-plugin-sdk-go/data" 9 | ) 10 | 11 | // WideFrame is a time series format where all the series live in one frame. 12 | // This time series format should be use for data that natively uses Labels and 13 | // when all of the series are guaranteed to have identical time values. 14 | type WideFrame []*data.Frame 15 | 16 | var WideFrameVersionLatest = WideFrameVersions()[len(WideFrameVersions())-1] 17 | 18 | func WideFrameVersions() []data.FrameTypeVersion { 19 | return []data.FrameTypeVersion{{0, 1}} 20 | } 21 | 22 | func NewWideFrame(refID string, v data.FrameTypeVersion) (*WideFrame, error) { 23 | if v.Greater(WideFrameVersionLatest) { 24 | return nil, fmt.Errorf("can not create WideFrame of version %s because it is newer than library version %v", v, WideFrameVersionLatest) 25 | } 26 | f := data.NewFrame("") 27 | f.RefID = refID 28 | f.SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesWide, TypeVersion: v}) 29 | return &WideFrame{f}, nil 30 | } 31 | 32 | func (wf *WideFrame) SetTime(timeName string, t []time.Time) error { 33 | switch { 34 | case wf == nil: 35 | return fmt.Errorf("wf is nil, NewWideFrame must be called first") 36 | case len(*wf) == 0: 37 | return fmt.Errorf("missing frame, NewWideFrame must be called first") 38 | case len(*wf) > 1: 39 | return fmt.Errorf("may not set time after adding extra frames") 40 | } 41 | 42 | frame := (*wf)[0] 43 | 44 | switch { 45 | case t == nil: 46 | return fmt.Errorf("t may not be nil") 47 | case frame.Fields != nil: 48 | return fmt.Errorf("expected fields property to be nil (metrics added before calling SetTime?)") 49 | case frame == nil: 50 | return fmt.Errorf("missing is nil, NewWideFrame must be called first") 51 | } 52 | 53 | frame.Fields = append(frame.Fields, data.NewField(timeName, nil, t)) 54 | return nil 55 | } 56 | 57 | func (wf *WideFrame) AddSeries(metricName string, values interface{}, l data.Labels, config *data.FieldConfig) error { 58 | if !data.ValidFieldType(values) { 59 | return fmt.Errorf("type %T is not a valid data frame field type", values) 60 | } 61 | 62 | switch { 63 | case wf == nil: 64 | return fmt.Errorf("wf is nil, NewWideFrame must be called first") 65 | case len(*wf) == 0: 66 | return fmt.Errorf("missing frame, NewWideFrame must be called first") 67 | case len(*wf) > 1: 68 | return fmt.Errorf("may not add metrics after adding extra frames") 69 | } 70 | 71 | frame := (*wf)[0] 72 | 73 | if frame == nil { 74 | return fmt.Errorf("missing is nil, NewWideFrame must be called first") 75 | } 76 | 77 | // Note: Readers are not required to make the Time field first, but using New/SetTime/AddSeries does. 78 | if len(frame.Fields) == 0 || frame.Fields[0].Type() != data.FieldTypeTime { 79 | return fmt.Errorf("frame is missing time field or time field is not first, SetTime must be called first") 80 | } 81 | 82 | valueField := data.NewField(metricName, l, values) 83 | 84 | if valueField.Len() != frame.Fields[0].Len() { 85 | return fmt.Errorf("value field length must match time field length, but got length %v for time and %v for values", 86 | frame.Fields[0].Len(), valueField.Len()) 87 | } 88 | 89 | valueField.Config = config 90 | 91 | frame.Fields = append(frame.Fields, valueField) 92 | 93 | return nil 94 | } 95 | 96 | func (wf *WideFrame) GetCollection(validateData bool) (Collection, error) { 97 | return validateAndGetRefsWide(wf, validateData) 98 | } 99 | 100 | func (wf *WideFrame) SetMetricMD(metricName string, l data.Labels, fc data.FieldConfig) { 101 | panic("not implemented") 102 | } 103 | 104 | func validateAndGetRefsWide(wf *WideFrame, validateData bool) (Collection, error) { 105 | var c Collection 106 | metricIndex := make(map[[2]string]struct{}) 107 | 108 | switch { 109 | case wf == nil: 110 | return c, fmt.Errorf("frames may not be nil") 111 | case len(*wf) == 0: 112 | return c, fmt.Errorf("missing frame, must be at least one frame") 113 | } 114 | 115 | frame := (*wf)[0] 116 | 117 | if frame == nil { 118 | return c, fmt.Errorf("frame is nil which is invalid") 119 | } 120 | 121 | c.RefID = frame.RefID 122 | 123 | if !frameHasType(frame, data.FrameTypeTimeSeriesWide) { 124 | return c, fmt.Errorf("frame has wrong type, expected TimeSeriesWide but got %q", frame.Meta.Type) 125 | } 126 | 127 | if frame.Meta.TypeVersion != WideFrameVersionLatest { 128 | c.Warning = &sdata.VersionWarning{DataVersion: frame.Meta.TypeVersion, LibraryVersion: WideFrameVersionLatest, DataType: data.FrameTypeTimeSeriesWide} 129 | } 130 | 131 | if len(frame.Fields) == 0 { // TODO: Error differently if nil and not zero length? 132 | if err := ignoreAdditionalFrames("additional frame on empty response", *wf, &c.RemainderIndices); err != nil { 133 | return c, err 134 | } 135 | c.Refs = []MetricRef{} 136 | return c, nil // empty response 137 | } 138 | 139 | if err := malformedFrameCheck(0, frame); err != nil { 140 | return c, err 141 | } 142 | 143 | timeField, ignoredTimedFields, err := seriesCheckSelectTime(0, frame) 144 | if err != nil { 145 | return c, err 146 | } 147 | if ignoredTimedFields != nil { 148 | c.RemainderIndices = append(c.RemainderIndices, ignoredTimedFields...) 149 | } 150 | 151 | valueFieldIndices := frame.TypeIndices(sdata.ValidValueFields()...) 152 | if len(valueFieldIndices) == 0 { 153 | return c, fmt.Errorf("frame is missing a numeric value field") 154 | } 155 | 156 | // TODO this is fragile if new types are added 157 | otherFields := frame.TypeIndices(data.FieldTypeNullableTime, data.FieldTypeString, data.FieldTypeNullableString) 158 | for _, fieldIdx := range otherFields { 159 | c.RemainderIndices = append(c.RemainderIndices, sdata.FrameFieldIndex{ 160 | FrameIdx: 0, FieldIdx: fieldIdx, 161 | Reason: fmt.Sprintf("unsupported field type %v", frame.Fields[fieldIdx].Type())}) 162 | } 163 | 164 | for _, vFieldIdx := range valueFieldIndices { 165 | vField := frame.Fields[vFieldIdx] 166 | if validateData { 167 | metricKey := [2]string{vField.Name, vField.Labels.String()} 168 | if _, ok := metricIndex[metricKey]; ok && validateData { 169 | return c, fmt.Errorf("duplicate metrics found for metric name %q and labels %q", vField.Name, vField.Labels) 170 | } 171 | metricIndex[metricKey] = struct{}{} 172 | } 173 | c.Refs = append(c.Refs, MetricRef{ 174 | TimeField: timeField, 175 | ValueField: vField, 176 | }) 177 | } 178 | 179 | // Validate time Field is sorted in ascending (oldest to newest) order 180 | if validateData { 181 | sorted, err := timeIsSorted(timeField) 182 | if err != nil { 183 | return c, fmt.Errorf("frame has an malformed time field") 184 | } 185 | if !sorted { 186 | return c, fmt.Errorf("frame has an unsorted time field") 187 | } 188 | } 189 | 190 | if err := ignoreAdditionalFrames("additional frame", *wf, &c.RemainderIndices); err != nil { 191 | return c, err 192 | } 193 | 194 | sortTimeSeriesMetricRef(c.Refs) 195 | return c, nil 196 | } 197 | 198 | func (wf *WideFrame) Frames() data.Frames { 199 | if wf == nil { 200 | return nil 201 | } 202 | return data.Frames(*wf) 203 | } 204 | --------------------------------------------------------------------------------