The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/core/services/backend_srv.ts

390 lines
11 KiB

import _ from 'lodash';
import angular from 'angular';
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import config from 'app/core/config';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { DashboardSearchHit } from 'app/types/search';
import { ContextSrv } from './context_srv';
import { FolderInfo, DashboardDTO, CoreEvents } from 'app/types';
import { BackendSrv as BackendService, getBackendSrv as getBackendService, BackendSrvRequest } from '@grafana/runtime';
import { AppEvents } from '@grafana/data';
export class BackendSrv implements BackendService {
private inFlightRequests: { [key: string]: Array<angular.IDeferred<any>> } = {};
private HTTP_REQUEST_CANCELED = -1;
private noBackendCache: boolean;
/** @ngInject */
constructor(
private $http: any,
private $q: angular.IQService,
private $timeout: angular.ITimeoutService,
private contextSrv: ContextSrv
) {}
get(url: string, params?: any) {
return this.request({ method: 'GET', url, params });
}
delete(url: string) {
return this.request({ method: 'DELETE', url });
}
post(url: string, data?: any) {
return this.request({ method: 'POST', url, data });
}
patch(url: string, data: any) {
return this.request({ method: 'PATCH', url, data });
}
put(url: string, data: any) {
return this.request({ method: 'PUT', url, data });
}
withNoBackendCache(callback: any) {
this.noBackendCache = true;
return callback().finally(() => {
this.noBackendCache = false;
});
}
requestErrorHandler(err: any) {
if (err.isHandled) {
return;
}
let data = err.data || { message: 'Unexpected error' };
if (_.isString(data)) {
data = { message: data };
}
if (err.status === 422) {
appEvents.emit(AppEvents.alertWarning, ['Validation failed', data.message]);
throw data;
}
if (data.message) {
let description = '';
let message = data.message;
if (message.length > 80) {
description = message;
message = 'Error';
}
appEvents.emit(err.status < 500 ? AppEvents.alertWarning : AppEvents.alertError, [message, description]);
}
throw data;
}
request(options: BackendSrvRequest) {
options.retry = options.retry || 0;
const requestIsLocal = !options.url.match(/^http/);
const firstAttempt = options.retry === 0;
if (requestIsLocal) {
if (this.contextSrv.user && this.contextSrv.user.orgId) {
options.headers = options.headers || {};
options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId;
}
if (options.url.indexOf('/') === 0) {
options.url = options.url.substring(1);
}
}
return this.$http(options).then(
(results: any) => {
if (options.method !== 'GET') {
if (results && results.data.message) {
if (options.showSuccessAlert !== false) {
appEvents.emit(AppEvents.alertSuccess, [results.data.message]);
}
}
}
return results.data;
},
(err: any) => {
// handle unauthorized
if (err.status === 401 && this.contextSrv.user.isSignedIn && firstAttempt) {
return this.loginPing()
.then(() => {
options.retry = 1;
return this.request(options);
})
.catch((err: any) => {
if (err.status === 401) {
window.location.href = config.appSubUrl + '/logout';
throw err;
}
});
}
this.$timeout(this.requestErrorHandler.bind(this, err), 50);
throw err;
}
);
}
addCanceler(requestId: string, canceler: angular.IDeferred<any>) {
if (requestId in this.inFlightRequests) {
this.inFlightRequests[requestId].push(canceler);
} else {
this.inFlightRequests[requestId] = [canceler];
}
}
resolveCancelerIfExists(requestId: string) {
const cancelers = this.inFlightRequests[requestId];
if (!_.isUndefined(cancelers) && cancelers.length) {
cancelers[0].resolve();
}
}
datasourceRequest(options: BackendSrvRequest) {
let canceler: angular.IDeferred<any> = null;
options.retry = options.retry || 0;
// A requestID is provided by the datasource as a unique identifier for a
// particular query. If the requestID exists, the promise it is keyed to
// is canceled, canceling the previous datasource request if it is still
// in-flight.
const requestId = options.requestId;
QueryProcessing: Observable query interface and RxJS for query & stream processing (#18899) * I needed to learn some rxjs and understand this more, so just playing around * Updated * Removed all the complete calls * Refactoring * StreamHandler -> observable start * progress * simple singal works * Handle update time range * added error handling * wrap old function * minor changes * handle data format in the subscribe function * Use replay subject to return last value to subscribers * Set loading state after no response in 50ms * added missing file * updated comment * Added cancelation of network requests * runRequest: Added unit test scenario framework * Progress on tests * minor refactor of unit tests * updated test * removed some old code * Shared queries work again, and also became so much simplier * unified query and observe methods * implict any fix * Fixed closed subject issue * removed comment * Use last returned data for loading state * WIP: Explore to runRequest makover step1 * Minor progress * Minor progress on explore and runRequest * minor progress * Things are starting to work in explore * Updated prometheus to use new observable query response, greatly simplified code * Revert refId change * Found better solution for key/refId/requestId problem * use observable with loki * tests compile * fix loki query prep * Explore: correct first response handling * Refactorings * Refactoring * Explore: Fixes LoadingState and GraphResults between runs (#18986) * Refactor: Adds state to DataQueryResponse * Fix: Fixes so we do not empty results before new data arrives Fixes: #17409 * Transformations work * observable test data * remove single() from loki promise * Fixed comment * Explore: Fixes failing Loki and Prometheus unit tests (#18995) * Tests: Makes datasource tests work again * Fix: Fixes loki datasource so highligthing works * Chore: Runs Prettier * Fixed query runner tests * Delay loading state indication to 200ms * Fixed test * fixed unit tests * Clear cached calcs * Fixed bug getProcesedDataFrames * Fix the correct test is a better idea * Fix: Fixes so queries in Explore are only run if Graph/Table is shown (#19000) * Fix: Fixes so queries in Explore are only run if Graph/Table is shown Fixes: #18618 * Refactor: Removes unnecessary condition * PanelData: provide legacy data only when needed (#19018) * no legacy * invert logic... now compiles * merge getQueryResponseData and getDataRaw * update comment about query editor * use single getData() function * only send legacy when it is used in explore * pre process rather than post process * pre process rather than post process * Minor refactoring * Add missing tags to test datasource response * MixedDatasource: Adds query observable pattern to MixedDatasource (#19037) * start mixed datasource * Refactor: Refactors into observable parttern * Tests: Fixes tests * Tests: Removes console.log * Refactor: Adds unique requestId
6 years ago
if (requestId) {
this.resolveCancelerIfExists(requestId);
// create new canceler
canceler = this.$q.defer();
options.timeout = canceler.promise;
this.addCanceler(requestId, canceler);
}
const requestIsLocal = !options.url.match(/^http/);
const firstAttempt = options.retry === 0;
if (requestIsLocal) {
if (this.contextSrv.user && this.contextSrv.user.orgId) {
options.headers = options.headers || {};
options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId;
}
if (options.url.indexOf('/') === 0) {
options.url = options.url.substring(1);
}
if (options.headers && options.headers.Authorization) {
options.headers['X-DS-Authorization'] = options.headers.Authorization;
delete options.headers.Authorization;
}
if (this.noBackendCache) {
options.headers['X-Grafana-NoCache'] = 'true';
}
}
return this.$http(options)
.then((response: any) => {
if (!options.silent) {
appEvents.emit(CoreEvents.dsRequestResponse, response);
}
return response;
})
.catch((err: any) => {
if (err.status === this.HTTP_REQUEST_CANCELED) {
throw { err, cancelled: true };
}
// handle unauthorized for backend requests
if (requestIsLocal && firstAttempt && err.status === 401) {
return this.loginPing()
.then(() => {
options.retry = 1;
if (canceler) {
canceler.resolve();
}
return this.datasourceRequest(options);
})
.catch((err: any) => {
if (err.status === 401) {
window.location.href = config.appSubUrl + '/logout';
throw err;
}
});
}
// populate error obj on Internal Error
if (_.isString(err.data) && err.status === 500) {
err.data = {
error: err.statusText,
response: err.data,
};
}
// for Prometheus
if (err.data && !err.data.message && _.isString(err.data.error)) {
err.data.message = err.data.error;
}
if (!options.silent) {
appEvents.emit(CoreEvents.dsRequestError, err);
}
throw err;
})
.finally(() => {
// clean up
if (options.requestId) {
this.inFlightRequests[options.requestId].shift();
}
});
}
loginPing() {
return this.request({ url: '/api/login/ping', method: 'GET', retry: 1 });
}
search(query: any): Promise<DashboardSearchHit[]> {
return this.get('/api/search', query);
}
getDashboardBySlug(slug: string) {
return this.get(`/api/dashboards/db/${slug}`);
}
getDashboardByUid(uid: string) {
return this.get(`/api/dashboards/uid/${uid}`);
}
getFolderByUid(uid: string) {
return this.get(`/api/folders/${uid}`);
}
saveDashboard(
dash: DashboardModel,
{ message = '', folderId, overwrite = false }: { message?: string; folderId?: number; overwrite?: boolean } = {}
) {
return this.post('/api/dashboards/db/', {
dashboard: dash,
folderId,
overwrite,
message,
});
}
createFolder(payload: any) {
return this.post('/api/folders', payload);
}
deleteFolder(uid: string, showSuccessAlert: boolean) {
return this.request({ method: 'DELETE', url: `/api/folders/${uid}`, showSuccessAlert: showSuccessAlert === true });
}
deleteDashboard(uid: string, showSuccessAlert: boolean) {
return this.request({
method: 'DELETE',
url: `/api/dashboards/uid/${uid}`,
showSuccessAlert: showSuccessAlert === true,
});
}
deleteFoldersAndDashboards(folderUids: string[], dashboardUids: string[]) {
const tasks = [];
for (const folderUid of folderUids) {
tasks.push(this.createTask(this.deleteFolder.bind(this), true, folderUid, true));
}
for (const dashboardUid of dashboardUids) {
tasks.push(this.createTask(this.deleteDashboard.bind(this), true, dashboardUid, true));
}
return this.executeInOrder(tasks, []);
}
moveDashboards(dashboardUids: string[], toFolder: FolderInfo) {
const tasks = [];
for (const uid of dashboardUids) {
tasks.push(this.createTask(this.moveDashboard.bind(this), true, uid, toFolder));
}
return this.executeInOrder(tasks, []).then((result: any) => {
return {
totalCount: result.length,
successCount: _.filter(result, { succeeded: true }).length,
alreadyInFolderCount: _.filter(result, { alreadyInFolder: true }).length,
};
});
}
private moveDashboard(uid: string, toFolder: FolderInfo) {
const deferred = this.$q.defer();
this.getDashboardByUid(uid).then((fullDash: DashboardDTO) => {
const model = new DashboardModel(fullDash.dashboard, fullDash.meta);
if ((!fullDash.meta.folderId && toFolder.id === 0) || fullDash.meta.folderId === toFolder.id) {
deferred.resolve({ alreadyInFolder: true });
return;
}
const clone = model.getSaveModelClone();
const options = {
folderId: toFolder.id,
overwrite: false,
};
this.saveDashboard(clone, options)
.then(() => {
deferred.resolve({ succeeded: true });
})
.catch((err: any) => {
if (err.data && err.data.status === 'plugin-dashboard') {
err.isHandled = true;
options.overwrite = true;
this.saveDashboard(clone, options)
.then(() => {
deferred.resolve({ succeeded: true });
})
.catch((err: any) => {
deferred.resolve({ succeeded: false });
});
} else {
deferred.resolve({ succeeded: false });
}
});
});
return deferred.promise;
}
private createTask(fn: Function, ignoreRejections: boolean, ...args: any[]) {
return (result: any) => {
return fn
.apply(null, args)
.then((res: any) => {
return Array.prototype.concat(result, [res]);
})
.catch((err: any) => {
if (ignoreRejections) {
return result;
}
throw err;
});
};
}
private executeInOrder(tasks: any[], initialValue: any[]) {
return tasks.reduce(this.$q.when, initialValue);
}
}
coreModule.service('backendSrv', BackendSrv);
// Used for testing and things that really need BackendSrv
export function getBackendSrv(): BackendSrv {
return getBackendService() as BackendSrv;
}