datasource.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import { identity, omit, pick, pickBy } from 'lodash';
  2. import { lastValueFrom, Observable, of } from 'rxjs';
  3. import { catchError, map } from 'rxjs/operators';
  4. import {
  5. DataQueryRequest,
  6. DataQueryResponse,
  7. DataSourceApi,
  8. DataSourceInstanceSettings,
  9. DataSourceJsonData,
  10. dateMath,
  11. DateTime,
  12. FieldType,
  13. MutableDataFrame,
  14. } from '@grafana/data';
  15. import { BackendSrvRequest, getBackendSrv, getTemplateSrv } from '@grafana/runtime';
  16. import { NodeGraphOptions } from 'app/core/components/NodeGraphSettings';
  17. import { serializeParams } from 'app/core/utils/fetch';
  18. import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
  19. import { ALL_OPERATIONS_KEY } from './components/SearchForm';
  20. import { createGraphFrames } from './graphTransform';
  21. import { createTableFrame, createTraceFrame } from './responseTransform';
  22. import { JaegerQuery } from './types';
  23. import { convertTagsLogfmt } from './util';
  24. export interface JaegerJsonData extends DataSourceJsonData {
  25. nodeGraph?: NodeGraphOptions;
  26. }
  27. export class JaegerDatasource extends DataSourceApi<JaegerQuery, JaegerJsonData> {
  28. uploadedJson: string | ArrayBuffer | null = null;
  29. nodeGraph?: NodeGraphOptions;
  30. constructor(
  31. private instanceSettings: DataSourceInstanceSettings<JaegerJsonData>,
  32. private readonly timeSrv: TimeSrv = getTimeSrv()
  33. ) {
  34. super(instanceSettings);
  35. this.nodeGraph = instanceSettings.jsonData.nodeGraph;
  36. }
  37. async metadataRequest(url: string, params?: Record<string, any>): Promise<any> {
  38. const res = await lastValueFrom(this._request(url, params, { hideFromInspector: true }));
  39. return res.data.data;
  40. }
  41. query(options: DataQueryRequest<JaegerQuery>): Observable<DataQueryResponse> {
  42. // At this moment we expect only one target. In case we somehow change the UI to be able to show multiple
  43. // traces at one we need to change this.
  44. const target: JaegerQuery = options.targets[0];
  45. if (!target) {
  46. return of({ data: [emptyTraceDataFrame] });
  47. }
  48. if (target.queryType !== 'search' && target.query) {
  49. return this._request(
  50. `/api/traces/${encodeURIComponent(getTemplateSrv().replace(target.query, options.scopedVars))}`
  51. ).pipe(
  52. map((response) => {
  53. const traceData = response?.data?.data?.[0];
  54. if (!traceData) {
  55. return { data: [emptyTraceDataFrame] };
  56. }
  57. let data = [createTraceFrame(traceData)];
  58. if (this.nodeGraph?.enabled) {
  59. data.push(...createGraphFrames(traceData));
  60. }
  61. return {
  62. data,
  63. };
  64. })
  65. );
  66. }
  67. if (target.queryType === 'upload') {
  68. if (!this.uploadedJson) {
  69. return of({ data: [] });
  70. }
  71. try {
  72. const traceData = JSON.parse(this.uploadedJson as string).data[0];
  73. let data = [createTraceFrame(traceData)];
  74. if (this.nodeGraph?.enabled) {
  75. data.push(...createGraphFrames(traceData));
  76. }
  77. return of({ data });
  78. } catch (error) {
  79. return of({ error: { message: 'JSON is not valid Jaeger format' }, data: [] });
  80. }
  81. }
  82. let jaegerQuery = pick(target, ['operation', 'service', 'tags', 'minDuration', 'maxDuration', 'limit']);
  83. // remove empty properties
  84. jaegerQuery = pickBy(jaegerQuery, identity);
  85. if (jaegerQuery.tags) {
  86. jaegerQuery = {
  87. ...jaegerQuery,
  88. tags: convertTagsLogfmt(getTemplateSrv().replace(jaegerQuery.tags, options.scopedVars)),
  89. };
  90. }
  91. if (jaegerQuery.operation === ALL_OPERATIONS_KEY) {
  92. jaegerQuery = omit(jaegerQuery, 'operation');
  93. }
  94. // TODO: this api is internal, used in jaeger ui. Officially they have gRPC api that should be used.
  95. return this._request(`/api/traces`, {
  96. ...jaegerQuery,
  97. ...this.getTimeRange(),
  98. lookback: 'custom',
  99. }).pipe(
  100. map((response) => {
  101. return {
  102. data: [createTableFrame(response.data.data, this.instanceSettings)],
  103. };
  104. })
  105. );
  106. }
  107. async testDatasource(): Promise<any> {
  108. return lastValueFrom(
  109. this._request('/api/services').pipe(
  110. map((res) => {
  111. const values: any[] = res?.data?.data || [];
  112. const testResult =
  113. values.length > 0
  114. ? { status: 'success', message: 'Data source connected and services found.' }
  115. : {
  116. status: 'error',
  117. message:
  118. 'Data source connected, but no services received. Verify that Jaeger is configured properly.',
  119. };
  120. return testResult;
  121. }),
  122. catchError((err: any) => {
  123. let message = 'Jaeger: ';
  124. if (err.statusText) {
  125. message += err.statusText;
  126. } else {
  127. message += 'Cannot connect to Jaeger';
  128. }
  129. if (err.status) {
  130. message += `. ${err.status}`;
  131. }
  132. if (err.data && err.data.message) {
  133. message += `. ${err.data.message}`;
  134. } else if (err.data) {
  135. message += `. ${JSON.stringify(err.data)}`;
  136. }
  137. return of({ status: 'error', message: message });
  138. })
  139. )
  140. );
  141. }
  142. getTimeRange(): { start: number; end: number } {
  143. const range = this.timeSrv.timeRange();
  144. return {
  145. start: getTime(range.from, false),
  146. end: getTime(range.to, true),
  147. };
  148. }
  149. getQueryDisplayText(query: JaegerQuery) {
  150. return query.query || '';
  151. }
  152. private _request(apiUrl: string, data?: any, options?: Partial<BackendSrvRequest>): Observable<Record<string, any>> {
  153. const params = data ? serializeParams(data) : '';
  154. const url = `${this.instanceSettings.url}${apiUrl}${params.length ? `?${params}` : ''}`;
  155. const req = {
  156. ...options,
  157. url,
  158. };
  159. return getBackendSrv().fetch(req);
  160. }
  161. }
  162. function getTime(date: string | DateTime, roundUp: boolean) {
  163. if (typeof date === 'string') {
  164. date = dateMath.parse(date, roundUp)!;
  165. }
  166. return date.valueOf() * 1000;
  167. }
  168. const emptyTraceDataFrame = new MutableDataFrame({
  169. fields: [
  170. {
  171. name: 'trace',
  172. type: FieldType.trace,
  173. values: [],
  174. },
  175. ],
  176. meta: {
  177. preferredVisualisationType: 'trace',
  178. custom: {
  179. traceFormat: 'jaeger',
  180. },
  181. },
  182. });