Commit 92ab4d80 by Ryan McKinley Committed by GitHub

Error Handling: support errors and data in a response (#20169)

parent 7a3d1c0e
...@@ -377,6 +377,11 @@ export interface DataQueryResponse { ...@@ -377,6 +377,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
*/ */
......
...@@ -284,6 +284,26 @@ func init() { ...@@ -284,6 +284,26 @@ func init() {
}) })
registerScenario(&Scenario{ 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{
Id: "logs", Id: "logs",
Name: "Logs", Name: "Logs",
......
...@@ -6,6 +6,7 @@ import { Tooltip, PopoverContent } from '@grafana/ui'; ...@@ -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> { ...@@ -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> { ...@@ -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 ...@@ -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 ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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'; ...@@ -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) => { ...@@ -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) => { ...@@ -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 { ...@@ -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> { ...@@ -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> { ...@@ -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 { ...@@ -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) {
......
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