Commit bf187044 by Ryan McKinley Committed by GitHub

Inspector: support custom metadata display (#20854)

parent 0bf9e9bc
......@@ -19,6 +19,13 @@ export interface QueryResultMeta {
// Used in Explore to show limit applied to search result
limit?: number;
// HACK: save the datassource name in the meta so we can load it from the response
// we should be able to find the datasource from the refId
datasource?: string;
// DatasSource Specific Values
custom?: Record<string, any>;
}
export interface QueryResultBase {
......
......@@ -91,6 +91,11 @@ export class DataSourcePlugin<
return this;
}
setMetadataInspector(MetadataInspector: ComponentType<MetadataInspectorProps<DSType, TQuery, TOptions>>) {
this.components.MetadataInspector = MetadataInspector;
return this;
}
setComponentsFromLegacyExports(pluginExports: any) {
this.angularConfigCtrl = pluginExports.ConfigCtrl;
......@@ -137,6 +142,7 @@ export interface DataSourcePluginComponents<
ExploreLogsQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
ExploreStartPage?: ComponentType<ExploreStartPageProps>;
ConfigEditor?: ComponentType<DataSourcePluginOptionsEditorProps<TOptions>>;
MetadataInspector?: ComponentType<MetadataInspectorProps<DSType, TQuery, TOptions>>;
}
// Only exported for tests
......@@ -331,6 +337,17 @@ export function updateDatasourcePluginResetKeyOption(props: DataSourcePluginOpti
props.onOptionsChange(config);
}
export interface MetadataInspectorProps<
DSType extends DataSourceApi<TQuery, TOptions>,
TQuery extends DataQuery = DataQuery,
TOptions extends DataSourceJsonData = DataSourceJsonData
> {
datasource: DSType;
// All Data from this DataSource
data: DataFrame[];
}
export interface QueryEditorProps<
DSType extends DataSourceApi<TQuery, TOptions>,
TQuery extends DataQuery = DataQuery,
......
// Libraries
import React, { PureComponent } from 'react';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { Drawer, JSONFormatter } from '@grafana/ui';
import { css } from 'emotion';
import { getLocationSrv } from '@grafana/runtime';
import { JSONFormatter, Drawer, Select, Table } from '@grafana/ui';
import { getLocationSrv, getDataSourceSrv } from '@grafana/runtime';
import { DataFrame, DataSourceApi, SelectableValue, applyFieldOverrides } from '@grafana/data';
import { config } from 'app/core/config';
interface Props {
dashboard: DashboardModel;
panel: PanelModel;
}
export class PanelInspector extends PureComponent<Props> {
enum InspectTab {
Data = 'data',
Raw = 'raw',
Issue = 'issue',
Meta = 'meta', // When result metadata exists
}
interface State {
// The last raw response
last?: any;
// Data frem the last response
data: DataFrame[];
// The selected data frame
selected: number;
// The Selected Tab
tab: InspectTab;
// If the datasource supports custom metadata
metaDS?: DataSourceApi;
}
export class PanelInspector extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
data: [],
selected: 0,
tab: InspectTab.Data,
};
}
async componentDidMount() {
const { panel } = this.props;
if (!panel) {
this.onDismiss(); // Try to close the component
return;
}
// TODO? should we get the result with an observable once?
const lastResult = (panel.getQueryRunner() as any).lastResult;
if (!lastResult) {
this.onDismiss(); // Usually opened from refresh?
return;
}
// Find the first DataSource wanting to show custom metadata
let metaDS: DataSourceApi;
const data = lastResult?.series as DataFrame[];
if (data) {
for (const frame of data) {
const key = frame.meta?.datasource;
if (key) {
const ds = await getDataSourceSrv().get(key);
if (ds && ds.components.MetadataInspector) {
metaDS = ds;
break;
}
}
}
}
// Set last result, but no metadata inspector
this.setState({
last: lastResult,
data,
metaDS,
});
}
onDismiss = () => {
getLocationSrv().update({
query: { inspect: null },
......@@ -19,24 +90,98 @@ export class PanelInspector extends PureComponent<Props> {
});
};
onSelectTab = (item: SelectableValue<InspectTab>) => {
this.setState({ tab: item.value || InspectTab.Data });
};
onSelectedFrameChanged = (item: SelectableValue<number>) => {
this.setState({ selected: item.value || 0 });
};
renderMetadataInspector() {
const { metaDS, data } = this.state;
if (!metaDS || !metaDS.components?.MetadataInspector) {
return <div>No Metadata Inspector</div>;
}
return <metaDS.components.MetadataInspector datasource={metaDS} data={data} />;
}
renderDataTab() {
const { data, selected } = this.state;
if (!data || !data.length) {
return <div>No Data</div>;
}
const choices = data.map((frame, index) => {
return {
value: index,
label: `${frame.name} (${index})`,
};
});
// Apply dummy styles
const processed = applyFieldOverrides({
data,
theme: config.theme,
fieldOptions: { defaults: {}, overrides: [] },
replaceVariables: (value: string) => {
return value;
},
});
return (
<div>
{choices.length > 1 && (
<div>
<Select
options={choices}
value={choices.find(t => t.value === selected)}
onChange={this.onSelectedFrameChanged}
/>
</div>
)}
<div style={{ border: '1px solid #666' }}>
<Table width={330} height={400} data={processed[selected]} />
</div>
</div>
);
}
renderIssueTab() {
return <div>TODO: show issue form</div>;
}
render() {
const { panel } = this.props;
const { last, tab } = this.state;
if (!panel) {
this.onDismiss(); // Try to close the component
return null;
}
const bodyStyle = css`
max-height: 70vh;
overflow-y: scroll;
`;
// TODO? should we get the result with an observable once?
const data = (panel.getQueryRunner() as any).lastResult;
const tabs = [
{ label: 'Data', value: InspectTab.Data },
{ label: 'Issue', value: InspectTab.Issue },
{ label: 'Raw JSON', value: InspectTab.Raw },
];
if (this.state.metaDS) {
tabs.push({ label: 'Meta Data', value: InspectTab.Meta });
}
return (
<Drawer title={panel.title} onClose={this.onDismiss}>
<div className={bodyStyle}>
<JSONFormatter json={data} open={2} />
</div>
<Select options={tabs} value={tabs.find(t => t.value === tab)} onChange={this.onSelectTab} />
{tab === InspectTab.Data && this.renderDataTab()}
{tab === InspectTab.Meta && this.renderMetadataInspector()}
{tab === InspectTab.Issue && this.renderIssueTab()}
{tab === InspectTab.Raw && (
<div>
<JSONFormatter json={last} open={2} />
</div>
)}
</Drawer>
);
}
......
import React, { PureComponent } from 'react';
import { MetadataInspectorProps, DataFrame } from '@grafana/data';
import { GraphiteDatasource } from './datasource';
import { GraphiteQuery, GraphiteOptions, MetricTankMeta, MetricTankResultMeta } from './types';
import { parseSchemaRetentions } from './meta';
export type Props = MetadataInspectorProps<GraphiteDatasource, GraphiteQuery, GraphiteOptions>;
export interface State {
index: number;
}
export class MetricTankMetaInspector extends PureComponent<Props, State> {
state = { index: 0 };
renderInfo = (info: MetricTankResultMeta, frame: DataFrame) => {
const buckets = parseSchemaRetentions(info['schema-retentions']);
return (
<div>
<h3>Info</h3>
<table>
<tbody>
{buckets.map(row => (
<tr key={row.interval}>
<td>{row.interval} &nbsp;</td>
<td>{row.retention} &nbsp;</td>
<td>{row.chunkspan} &nbsp;</td>
<td>{row.numchunks} &nbsp;</td>
<td>{row.ready} &nbsp;</td>
</tr>
))}
</tbody>
</table>
<pre>{JSON.stringify(info, null, 2)}</pre>
</div>
);
};
render() {
const { data } = this.props;
if (!data || !data.length) {
return <div>No Metadata</div>;
}
const frame = data[this.state.index];
const meta = frame.meta?.custom as MetricTankMeta;
if (!meta || !meta.info) {
return <>No Metadatata on DataFrame</>;
}
return (
<div>
<h3>MetricTank Request</h3>
<pre>{JSON.stringify(meta.request, null, 2)}</pre>
{meta.info.map(info => this.renderInfo(info, frame))}
</div>
);
}
}
......@@ -103,7 +103,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
}
}
convertResponseToDataFrames(result: any): DataQueryResponse {
convertResponseToDataFrames = (result: any): DataQueryResponse => {
const data: DataFrame[] = [];
if (!result || !result.data) {
return { data };
......@@ -124,14 +124,17 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
// Metrictank metadata
if (s.meta) {
frame.meta = {
metrictank: s.meta, // array of metadata
metrictankReq: result.data.meta, // info on the request
datasource: this.name,
custom: {
request: result.data.meta, // info for the whole request
info: s.meta, // Array of metadata
},
};
}
data.push(frame);
}
return { data };
}
};
parseTags(tagString: string) {
let tags: string[] = [];
......
import { parseSchemaRetentions } from './meta';
describe('metadata parsing', () => {
it('should parse schema retentions', () => {
const retentions = '1s:35d:20min:5:1542274085,1min:38d:2h:1:true,10min:120d:6h:1:true,2h:2y:6h:2';
const info = parseSchemaRetentions(retentions);
expect(info).toMatchInlineSnapshot(`
Array [
Object {
"chunkspan": "20min",
"interval": "1s",
"numchunks": 5,
"ready": 1542274085,
"retention": "35d",
},
Object {
"chunkspan": "2h",
"interval": "1min",
"numchunks": 1,
"ready": true,
"retention": "38d",
},
Object {
"chunkspan": "6h",
"interval": "10min",
"numchunks": 1,
"ready": true,
"retention": "120d",
},
Object {
"chunkspan": "6h",
"interval": "2h",
"numchunks": 2,
"ready": undefined,
"retention": "2y",
},
]
`);
});
});
export interface MetricTankResultMeta {
'schema-name': string;
'schema-retentions': string; //"1s:35d:20min:5:1542274085,1min:38d:2h:1:true,10min:120d:6h:1:true,2h:2y:6h:2",
}
// https://github.com/grafana/metrictank/blob/master/scripts/config/storage-schemas.conf#L15-L46
export interface RetentionInfo {
interval: string;
retention?: string;
chunkspan?: string;
numchunks?: number;
ready?: boolean | number; // whether, or as of what data timestamp, the archive is ready for querying.
}
function toInteger(val?: string): number | undefined {
if (val) {
return parseInt(val, 10);
}
return undefined;
}
function toBooleanOrTimestamp(val?: string): number | boolean | undefined {
if (val) {
if (val === 'true') {
return true;
}
if (val === 'false') {
return false;
}
return parseInt(val, 10);
}
return undefined;
}
export function parseSchemaRetentions(spec: string): RetentionInfo[] {
if (!spec) {
return [];
}
return spec.split(',').map(str => {
const vals = str.split(':');
return {
interval: vals[0],
retention: vals[1],
chunkspan: vals[2],
numchunks: toInteger(vals[3]),
ready: toBooleanOrTimestamp(vals[4]),
};
});
}
......@@ -2,6 +2,7 @@ import { GraphiteDatasource } from './datasource';
import { GraphiteQueryCtrl } from './query_ctrl';
import { DataSourcePlugin } from '@grafana/data';
import { ConfigEditor } from './configuration/ConfigEditor';
import { MetricTankMetaInspector } from './MetricTankMetaInspector';
class AnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html';
......@@ -10,4 +11,5 @@ class AnnotationsQueryCtrl {
export const plugin = new DataSourcePlugin(GraphiteDatasource)
.setQueryCtrl(GraphiteQueryCtrl)
.setConfigEditor(ConfigEditor)
.setMetadataInspector(MetricTankMetaInspector)
.setAnnotationQueryCtrl(AnnotationsQueryCtrl);
......@@ -13,3 +13,24 @@ export enum GraphiteType {
Default = 'default',
Metrictank = 'metrictank',
}
export interface MetricTankRequestMeta {
[key: string]: any; // TODO -- fill this with real values from metrictank
}
export interface MetricTankResultMeta {
'schema-name': string;
'schema-retentions': string; //"1s:35d:20min:5:1542274085,1min:38d:2h:1:true,10min:120d:6h:1:true,2h:2y:6h:2",
'archive-read': number;
'archive-interval': number;
'aggnum-norm': number;
'consolidate-normfetch': string; //"MaximumConsolidator",
'aggnum-rc': number;
'consolidate-rc': string; //"MaximumConsolidator",
count: number;
}
export interface MetricTankMeta {
request: MetricTankRequestMeta;
info: MetricTankResultMeta[];
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment