datasource.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. // Libraries
  2. import _ from 'lodash';
  3. // Services & Utils
  4. import * as dateMath from '@grafana/ui/src/utils/datemath';
  5. import { addLabelToSelector } from 'app/plugins/datasource/prometheus/add_label_to_query';
  6. import LanguageProvider from './language_provider';
  7. import { logStreamToSeriesData } from './result_transformer';
  8. import { formatQuery, parseQuery, getHighlighterExpressionsFromQuery } from './query_utils';
  9. // Types
  10. import {
  11. PluginMeta,
  12. DataQueryRequest,
  13. SeriesData,
  14. DataSourceApi,
  15. DataSourceInstanceSettings,
  16. DataQueryError,
  17. } from '@grafana/ui/src/types';
  18. import { LokiQuery, LokiOptions } from './types';
  19. import { BackendSrv } from 'app/core/services/backend_srv';
  20. import { TemplateSrv } from 'app/features/templating/template_srv';
  21. import { safeStringifyValue } from 'app/core/utils/explore';
  22. import { LogRowModel } from 'app/core/logs_model';
  23. export const DEFAULT_MAX_LINES = 1000;
  24. const DEFAULT_QUERY_PARAMS = {
  25. direction: 'BACKWARD',
  26. limit: DEFAULT_MAX_LINES,
  27. regexp: '',
  28. query: '',
  29. };
  30. function serializeParams(data: any) {
  31. return Object.keys(data)
  32. .map(k => {
  33. const v = data[k];
  34. return encodeURIComponent(k) + '=' + encodeURIComponent(v);
  35. })
  36. .join('&');
  37. }
  38. export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
  39. languageProvider: LanguageProvider;
  40. maxLines: number;
  41. /** @ngInject */
  42. constructor(
  43. private instanceSettings: DataSourceInstanceSettings<LokiOptions>,
  44. private backendSrv: BackendSrv,
  45. private templateSrv: TemplateSrv
  46. ) {
  47. super(instanceSettings);
  48. this.languageProvider = new LanguageProvider(this);
  49. const settingsData = instanceSettings.jsonData || {};
  50. this.maxLines = parseInt(settingsData.maxLines, 10) || DEFAULT_MAX_LINES;
  51. }
  52. _request(apiUrl: string, data?, options?: any) {
  53. const baseUrl = this.instanceSettings.url;
  54. const params = data ? serializeParams(data) : '';
  55. const url = `${baseUrl}${apiUrl}?${params}`;
  56. const req = {
  57. ...options,
  58. url,
  59. };
  60. return this.backendSrv.datasourceRequest(req);
  61. }
  62. prepareQueryTarget(target: LokiQuery, options: DataQueryRequest<LokiQuery>) {
  63. const interpolated = this.templateSrv.replace(target.expr);
  64. const { query, regexp } = parseQuery(interpolated);
  65. const start = this.getTime(options.range.from, false);
  66. const end = this.getTime(options.range.to, true);
  67. const refId = target.refId;
  68. return {
  69. ...DEFAULT_QUERY_PARAMS,
  70. query,
  71. regexp,
  72. start,
  73. end,
  74. limit: this.maxLines,
  75. refId,
  76. };
  77. }
  78. async query(options: DataQueryRequest<LokiQuery>) {
  79. const queryTargets = options.targets
  80. .filter(target => target.expr && !target.hide)
  81. .map(target => this.prepareQueryTarget(target, options));
  82. if (queryTargets.length === 0) {
  83. return Promise.resolve({ data: [] });
  84. }
  85. const queries = queryTargets.map(target =>
  86. this._request('/api/prom/query', target).catch((err: any) => {
  87. if (err.cancelled) {
  88. return err;
  89. }
  90. const error: DataQueryError = {
  91. message: 'Unknown error during query transaction. Please check JS console logs.',
  92. refId: target.refId,
  93. };
  94. if (err.data) {
  95. if (typeof err.data === 'string') {
  96. error.message = err.data;
  97. } else if (err.data.error) {
  98. error.message = safeStringifyValue(err.data.error);
  99. }
  100. } else if (err.message) {
  101. error.message = err.message;
  102. } else if (typeof err === 'string') {
  103. error.message = err;
  104. }
  105. error.status = err.status;
  106. error.statusText = err.statusText;
  107. throw error;
  108. })
  109. );
  110. return Promise.all(queries).then((results: any[]) => {
  111. const series: Array<SeriesData | DataQueryError> = [];
  112. for (let i = 0; i < results.length; i++) {
  113. const result = results[i];
  114. if (result.data) {
  115. const refId = queryTargets[i].refId;
  116. for (const stream of result.data.streams || []) {
  117. const seriesData = logStreamToSeriesData(stream);
  118. seriesData.refId = refId;
  119. seriesData.meta = {
  120. searchWords: getHighlighterExpressionsFromQuery(
  121. formatQuery(queryTargets[i].query, queryTargets[i].regexp)
  122. ),
  123. limit: this.maxLines,
  124. };
  125. series.push(seriesData);
  126. }
  127. }
  128. }
  129. return { data: series };
  130. });
  131. }
  132. async importQueries(queries: LokiQuery[], originMeta: PluginMeta): Promise<LokiQuery[]> {
  133. return this.languageProvider.importQueries(queries, originMeta.id);
  134. }
  135. metadataRequest(url) {
  136. // HACK to get label values for {job=|}, will be replaced when implementing LokiQueryField
  137. const apiUrl = url.replace('v1', 'prom');
  138. return this._request(apiUrl, { silent: true }).then(res => {
  139. const data = { data: { data: res.data.values || [] } };
  140. return data;
  141. });
  142. }
  143. modifyQuery(query: LokiQuery, action: any): LokiQuery {
  144. const parsed = parseQuery(query.expr || '');
  145. let { query: selector } = parsed;
  146. switch (action.type) {
  147. case 'ADD_FILTER': {
  148. selector = addLabelToSelector(selector, action.key, action.value);
  149. break;
  150. }
  151. default:
  152. break;
  153. }
  154. const expression = formatQuery(selector, parsed.regexp);
  155. return { ...query, expr: expression };
  156. }
  157. getHighlighterExpression(query: LokiQuery): string[] {
  158. return getHighlighterExpressionsFromQuery(query.expr);
  159. }
  160. getTime(date, roundUp) {
  161. if (_.isString(date)) {
  162. date = dateMath.parse(date, roundUp);
  163. }
  164. return Math.ceil(date.valueOf() * 1e6);
  165. }
  166. prepareLogRowContextQueryTargets = (row: LogRowModel, limit: number) => {
  167. const query = Object.keys(row.labels)
  168. .map(label => {
  169. return `${label}="${row.labels[label]}"`;
  170. })
  171. .join(',');
  172. const contextTimeBuffer = 2 * 60 * 60 * 1000 * 1e6; // 2h buffer
  173. const timeEpochNs = row.timeEpochMs * 1e6;
  174. const commontTargetOptons = {
  175. limit,
  176. query: `{${query}}`,
  177. };
  178. return [
  179. // Target for "before" context
  180. {
  181. ...commontTargetOptons,
  182. start: timeEpochNs - contextTimeBuffer,
  183. end: timeEpochNs,
  184. direction: 'BACKWARD',
  185. },
  186. // Target for "after" context
  187. {
  188. ...commontTargetOptons,
  189. start: timeEpochNs, // TODO: We should add 1ns here for the original row not no be included in the result
  190. end: timeEpochNs + contextTimeBuffer,
  191. direction: 'FORWARD',
  192. },
  193. ];
  194. };
  195. getLogRowContext = (row: LogRowModel, limit?: number) => {
  196. // Preparing two targets, for preceeding and following log queries
  197. const targets = this.prepareLogRowContextQueryTargets(row, limit || 10);
  198. return Promise.all(
  199. targets.map(target => {
  200. return this._request('/api/prom/query', target).catch(e => {
  201. const error: DataQueryError = {
  202. message: 'Error during context query. Please check JS console logs.',
  203. status: e.status,
  204. statusText: e.statusText,
  205. };
  206. return error;
  207. });
  208. })
  209. ).then((results: any[]) => {
  210. const series: Array<Array<SeriesData | DataQueryError>> = [];
  211. const emptySeries = {
  212. fields: [],
  213. rows: [],
  214. } as SeriesData;
  215. for (let i = 0; i < results.length; i++) {
  216. const result = results[i];
  217. series[i] = [];
  218. if (result.data) {
  219. for (const stream of result.data.streams || []) {
  220. const seriesData = logStreamToSeriesData(stream);
  221. series[i].push(seriesData);
  222. }
  223. } else {
  224. series[i].push(result);
  225. }
  226. }
  227. // Following context logs are requested in "forward" direction.
  228. // This means, that we need to reverse those to make them sorted
  229. // in descending order (by timestamp)
  230. if (series[1][0] && (series[1][0] as SeriesData).rows) {
  231. (series[1][0] as SeriesData).rows.reverse();
  232. }
  233. return { data: [series[0][0] || emptySeries, series[1][0] || emptySeries] };
  234. });
  235. };
  236. testDatasource() {
  237. return this._request('/api/prom/label')
  238. .then(res => {
  239. if (res && res.data && res.data.values && res.data.values.length > 0) {
  240. return { status: 'success', message: 'Data source connected and labels found.' };
  241. }
  242. return {
  243. status: 'error',
  244. message:
  245. 'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.',
  246. };
  247. })
  248. .catch(err => {
  249. let message = 'Loki: ';
  250. if (err.statusText) {
  251. message += err.statusText;
  252. } else {
  253. message += 'Cannot connect to Loki';
  254. }
  255. if (err.status) {
  256. message += `. ${err.status}`;
  257. }
  258. if (err.data && err.data.message) {
  259. message += `. ${err.data.message}`;
  260. } else if (err.data) {
  261. message += `. ${err.data}`;
  262. }
  263. return { status: 'error', message: message };
  264. });
  265. }
  266. }
  267. export default LokiDatasource;