mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Error Handling: support errors and data in a response (#20169)
This commit is contained in:
@@ -376,6 +376,11 @@ export interface DataQueryResponse {
|
|||||||
*/
|
*/
|
||||||
key?: string;
|
key?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optionally include error info along with the response data
|
||||||
|
*/
|
||||||
|
error?: DataQueryError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use this to control which state the response should have
|
* Use this to control which state the response should have
|
||||||
* Defaults to LoadingState.Done if state is not defined
|
* Defaults to LoadingState.Done if state is not defined
|
||||||
|
@@ -283,6 +283,26 @@ func init() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
registerScenario(&Scenario{
|
||||||
|
Id: "random_walk_with_error",
|
||||||
|
Name: "Random Walk (with error)",
|
||||||
|
|
||||||
|
Handler: func(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.QueryResult {
|
||||||
|
queryRes := getRandomWalk(query, context)
|
||||||
|
queryRes.ErrorString = "This is an error. It can include URLs http://grafana.com/"
|
||||||
|
return queryRes
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
registerScenario(&Scenario{
|
||||||
|
Id: "server_error_500",
|
||||||
|
Name: "Server Error (500)",
|
||||||
|
|
||||||
|
Handler: func(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.QueryResult {
|
||||||
|
panic("Test Data Panic!")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
registerScenario(&Scenario{
|
registerScenario(&Scenario{
|
||||||
Id: "logs",
|
Id: "logs",
|
||||||
Name: "Logs",
|
Name: "Logs",
|
||||||
|
@@ -6,6 +6,7 @@ import { Tooltip, PopoverContent } from '@grafana/ui';
|
|||||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||||
import templateSrv from 'app/features/templating/template_srv';
|
import templateSrv from 'app/features/templating/template_srv';
|
||||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
|
import { getLocationSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
enum InfoMode {
|
enum InfoMode {
|
||||||
Error = 'Error',
|
Error = 'Error',
|
||||||
@@ -68,11 +69,18 @@ export class PanelHeaderCorner extends Component<Props> {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
renderCornerType(infoMode: InfoMode, content: PopoverContent) {
|
/**
|
||||||
|
* Open the Panel Inspector when we click on an error
|
||||||
|
*/
|
||||||
|
onClickError = () => {
|
||||||
|
getLocationSrv().update({ partial: true, query: { inspect: this.props.panel.id } });
|
||||||
|
};
|
||||||
|
|
||||||
|
renderCornerType(infoMode: InfoMode, content: PopoverContent, onClick?: () => void) {
|
||||||
const theme = infoMode === InfoMode.Error ? 'error' : 'info';
|
const theme = infoMode === InfoMode.Error ? 'error' : 'info';
|
||||||
return (
|
return (
|
||||||
<Tooltip content={content} placement="top-start" theme={theme}>
|
<Tooltip content={content} placement="top-start" theme={theme}>
|
||||||
<div className={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}>
|
<div className={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`} onClick={onClick}>
|
||||||
<i className="fa" />
|
<i className="fa" />
|
||||||
<span className="panel-info-corner-inner" />
|
<span className="panel-info-corner-inner" />
|
||||||
</div>
|
</div>
|
||||||
@@ -88,7 +96,7 @@ export class PanelHeaderCorner extends Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (infoMode === InfoMode.Error) {
|
if (infoMode === InfoMode.Error) {
|
||||||
return this.renderCornerType(infoMode, this.props.error);
|
return this.renderCornerType(infoMode, this.props.error, this.onClickError);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (infoMode === InfoMode.Info || infoMode === InfoMode.Links) {
|
if (infoMode === InfoMode.Info || infoMode === InfoMode.Links) {
|
||||||
|
@@ -38,6 +38,9 @@ export function processResponsePacket(packet: DataQueryResponse, state: RunningQ
|
|||||||
|
|
||||||
packets[packet.key || 'A'] = packet;
|
packets[packet.key || 'A'] = packet;
|
||||||
|
|
||||||
|
let loadingState = packet.state || LoadingState.Done;
|
||||||
|
let error: DataQueryError | undefined = undefined;
|
||||||
|
|
||||||
// Update the time range
|
// Update the time range
|
||||||
const range = { ...request.range };
|
const range = { ...request.range };
|
||||||
const timeRange = isString(range.raw.from)
|
const timeRange = isString(range.raw.from)
|
||||||
@@ -50,13 +53,18 @@ export function processResponsePacket(packet: DataQueryResponse, state: RunningQ
|
|||||||
|
|
||||||
const combinedData = flatten(
|
const combinedData = flatten(
|
||||||
lodashMap(packets, (packet: DataQueryResponse) => {
|
lodashMap(packets, (packet: DataQueryResponse) => {
|
||||||
|
if (packet.error) {
|
||||||
|
loadingState = LoadingState.Error;
|
||||||
|
error = packet.error;
|
||||||
|
}
|
||||||
return packet.data;
|
return packet.data;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const panelData = {
|
const panelData = {
|
||||||
state: packet.state || LoadingState.Done,
|
state: loadingState,
|
||||||
series: combinedData,
|
series: combinedData,
|
||||||
|
error,
|
||||||
request,
|
request,
|
||||||
timeRange,
|
timeRange,
|
||||||
};
|
};
|
||||||
|
@@ -108,18 +108,15 @@ class MetricsPanelCtrl extends PanelCtrl {
|
|||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.error = err.message || 'Request Error';
|
this.error = err.message || 'Request Error';
|
||||||
this.inspector = { error: err };
|
|
||||||
|
|
||||||
if (err.data) {
|
if (err.data) {
|
||||||
if (err.data.message) {
|
if (err.data.message) {
|
||||||
this.error = err.data.message;
|
this.error = err.data.message;
|
||||||
}
|
} else if (err.data.error) {
|
||||||
if (err.data.error) {
|
|
||||||
this.error = err.data.error;
|
this.error = err.data.error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Panel data error:', err);
|
|
||||||
return this.$timeout(() => {
|
return this.$timeout(() => {
|
||||||
this.events.emit(PanelEvents.dataError, err);
|
this.events.emit(PanelEvents.dataError, err);
|
||||||
});
|
});
|
||||||
@@ -131,7 +128,10 @@ class MetricsPanelCtrl extends PanelCtrl {
|
|||||||
if (data.state === LoadingState.Error) {
|
if (data.state === LoadingState.Error) {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.processDataError(data.error);
|
this.processDataError(data.error);
|
||||||
return;
|
if (!data.series) {
|
||||||
|
// keep current data if the response is empty
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore data in loading state
|
// Ignore data in loading state
|
||||||
|
@@ -31,7 +31,6 @@ export class PanelCtrl {
|
|||||||
$injector: auto.IInjectorService;
|
$injector: auto.IInjectorService;
|
||||||
$location: any;
|
$location: any;
|
||||||
$timeout: any;
|
$timeout: any;
|
||||||
inspector: any;
|
|
||||||
editModeInitiated: boolean;
|
editModeInitiated: boolean;
|
||||||
height: any;
|
height: any;
|
||||||
containerHeight: any;
|
containerHeight: any;
|
||||||
|
@@ -5,6 +5,7 @@ import Drop from 'tether-drop';
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import baron from 'baron';
|
import baron from 'baron';
|
||||||
import { PanelEvents } from '@grafana/data';
|
import { PanelEvents } from '@grafana/data';
|
||||||
|
import { getLocationSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
const module = angular.module('grafana.directives');
|
const module = angular.module('grafana.directives');
|
||||||
|
|
||||||
@@ -67,6 +68,12 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function infoCornerClicked() {
|
||||||
|
if (ctrl.error) {
|
||||||
|
getLocationSrv().update({ partial: true, query: { inspect: ctrl.panel.id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// set initial transparency
|
// set initial transparency
|
||||||
if (ctrl.panel.transparent) {
|
if (ctrl.panel.transparent) {
|
||||||
transparentLastState = true;
|
transparentLastState = true;
|
||||||
@@ -200,6 +207,8 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
|||||||
elem.on('mouseenter', mouseEnter);
|
elem.on('mouseenter', mouseEnter);
|
||||||
elem.on('mouseleave', mouseLeave);
|
elem.on('mouseleave', mouseLeave);
|
||||||
|
|
||||||
|
cornerInfoElem.on('click', infoCornerClicked);
|
||||||
|
|
||||||
scope.$on('$destroy', () => {
|
scope.$on('$destroy', () => {
|
||||||
elem.off();
|
elem.off();
|
||||||
cornerInfoElem.off();
|
cornerInfoElem.off();
|
||||||
|
@@ -6,6 +6,7 @@ import {
|
|||||||
MetricFindValue,
|
MetricFindValue,
|
||||||
TableData,
|
TableData,
|
||||||
TimeSeries,
|
TimeSeries,
|
||||||
|
DataQueryError,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { Scenario, TestDataQuery } from './types';
|
import { Scenario, TestDataQuery } from './types';
|
||||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
@@ -67,6 +68,7 @@ export class TestDataDataSource extends DataSourceApi<TestDataQuery> {
|
|||||||
|
|
||||||
processQueryResult(queries: any, res: any): DataQueryResponse {
|
processQueryResult(queries: any, res: any): DataQueryResponse {
|
||||||
const data: TestData[] = [];
|
const data: TestData[] = [];
|
||||||
|
let error: DataQueryError | undefined = undefined;
|
||||||
|
|
||||||
for (const query of queries) {
|
for (const query of queries) {
|
||||||
const results = res.data.results[query.refId];
|
const results = res.data.results[query.refId];
|
||||||
@@ -81,9 +83,15 @@ export class TestDataDataSource extends DataSourceApi<TestDataQuery> {
|
|||||||
for (const series of results.series || []) {
|
for (const series of results.series || []) {
|
||||||
data.push({ target: series.name, datapoints: series.points, refId: query.refId, tags: series.tags });
|
data.push({ target: series.name, datapoints: series.points, refId: query.refId, tags: series.tags });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (results.error) {
|
||||||
|
error = {
|
||||||
|
message: results.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { data };
|
return { data, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
annotationQuery(options: any) {
|
annotationQuery(options: any) {
|
||||||
|
@@ -480,13 +480,11 @@ class GraphElement {
|
|||||||
this.plot = $.plot(this.elem, this.sortedSeries, options);
|
this.plot = $.plot(this.elem, this.sortedSeries, options);
|
||||||
if (this.ctrl.renderError) {
|
if (this.ctrl.renderError) {
|
||||||
delete this.ctrl.error;
|
delete this.ctrl.error;
|
||||||
delete this.ctrl.inspector;
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('flotcharts error', e);
|
console.log('flotcharts error', e);
|
||||||
this.ctrl.error = e.message || 'Render Error';
|
this.ctrl.error = e.message || 'Render Error';
|
||||||
this.ctrl.renderError = true;
|
this.ctrl.renderError = true;
|
||||||
this.ctrl.inspector = { error: e };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (incrementRenderCounter) {
|
if (incrementRenderCounter) {
|
||||||
|
Reference in New Issue
Block a user