datasource.test.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. import { lastValueFrom, of, throwError } from 'rxjs';
  2. import { createFetchResponse } from 'test/helpers/createFetchResponse';
  3. import {
  4. DataQueryRequest,
  5. DataSourceInstanceSettings,
  6. dateTime,
  7. FieldType,
  8. PluginType,
  9. ScopedVars,
  10. } from '@grafana/data';
  11. import { backendSrv } from 'app/core/services/backend_srv';
  12. import { ALL_OPERATIONS_KEY } from './components/SearchForm';
  13. import { JaegerDatasource, JaegerJsonData } from './datasource';
  14. import mockJson from './mockJsonResponse.json';
  15. import {
  16. testResponse,
  17. testResponseDataFrameFields,
  18. testResponseEdgesFields,
  19. testResponseNodesFields,
  20. } from './testResponse';
  21. import { JaegerQuery } from './types';
  22. jest.mock('@grafana/runtime', () => ({
  23. ...(jest.requireActual('@grafana/runtime') as any),
  24. getBackendSrv: () => backendSrv,
  25. getTemplateSrv: () => ({
  26. replace: (val: string, subs: ScopedVars): string => {
  27. return subs[val]?.value ?? val;
  28. },
  29. }),
  30. }));
  31. const timeSrvStub: any = {
  32. timeRange(): any {
  33. return {
  34. from: dateTime(1531468681),
  35. to: dateTime(1531489712),
  36. };
  37. },
  38. };
  39. describe('JaegerDatasource', () => {
  40. beforeEach(() => {
  41. jest.clearAllMocks();
  42. });
  43. it('returns trace and graph when queried', async () => {
  44. setupFetchMock({ data: [testResponse] });
  45. const ds = new JaegerDatasource(defaultSettings);
  46. const response = await lastValueFrom(ds.query(defaultQuery));
  47. expect(response.data.length).toBe(3);
  48. expect(response.data[0].fields).toMatchObject(testResponseDataFrameFields);
  49. expect(response.data[1].fields).toMatchObject(testResponseNodesFields);
  50. expect(response.data[2].fields).toMatchObject(testResponseEdgesFields);
  51. });
  52. it('returns trace when traceId with special characters is queried', async () => {
  53. const mock = setupFetchMock({ data: [testResponse] });
  54. const ds = new JaegerDatasource(defaultSettings);
  55. const query = {
  56. ...defaultQuery,
  57. targets: [
  58. {
  59. query: 'a/b',
  60. refId: '1',
  61. },
  62. ],
  63. };
  64. await lastValueFrom(ds.query(query));
  65. expect(mock).toBeCalledWith({ url: `${defaultSettings.url}/api/traces/a%2Fb` });
  66. });
  67. it('returns empty response if trace id is not specified', async () => {
  68. const ds = new JaegerDatasource(defaultSettings);
  69. const response = await lastValueFrom(
  70. ds.query({
  71. ...defaultQuery,
  72. targets: [],
  73. })
  74. );
  75. const field = response.data[0].fields[0];
  76. expect(field.name).toBe('trace');
  77. expect(field.type).toBe(FieldType.trace);
  78. expect(field.values.length).toBe(0);
  79. });
  80. it('should handle json file upload', async () => {
  81. const ds = new JaegerDatasource(defaultSettings);
  82. ds.uploadedJson = JSON.stringify(mockJson);
  83. const response = await lastValueFrom(
  84. ds.query({
  85. ...defaultQuery,
  86. targets: [{ queryType: 'upload', refId: 'A' }],
  87. })
  88. );
  89. const field = response.data[0].fields[0];
  90. expect(field.name).toBe('traceID');
  91. expect(field.type).toBe(FieldType.string);
  92. expect(field.values.length).toBe(2);
  93. });
  94. it('should fail on invalid json file upload', async () => {
  95. const ds = new JaegerDatasource(defaultSettings);
  96. ds.uploadedJson = JSON.stringify({ key: 'value', arr: [] });
  97. const response = await lastValueFrom(
  98. ds.query({
  99. targets: [{ queryType: 'upload', refId: 'A' }],
  100. } as any)
  101. );
  102. expect(response.error?.message).toBeDefined();
  103. expect(response.data.length).toBe(0);
  104. });
  105. it('should return search results when the query type is search', async () => {
  106. const mock = setupFetchMock({ data: [testResponse] });
  107. const ds = new JaegerDatasource(defaultSettings, timeSrvStub);
  108. const response = await lastValueFrom(
  109. ds.query({
  110. ...defaultQuery,
  111. targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', operation: '/api/services' }],
  112. })
  113. );
  114. expect(mock).toBeCalledWith({
  115. url: `${defaultSettings.url}/api/traces?operation=%2Fapi%2Fservices&service=jaeger-query&start=1531468681000&end=1531489712000&lookback=custom`,
  116. });
  117. expect(response.data[0].meta.preferredVisualisationType).toBe('table');
  118. // Make sure that traceID field has data link configured
  119. expect(response.data[0].fields[0].config.links).toHaveLength(1);
  120. expect(response.data[0].fields[0].name).toBe('traceID');
  121. });
  122. it('should remove operation from the query when all is selected', async () => {
  123. const mock = setupFetchMock({ data: [testResponse] });
  124. const ds = new JaegerDatasource(defaultSettings, timeSrvStub);
  125. await lastValueFrom(
  126. ds.query({
  127. ...defaultQuery,
  128. targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', operation: ALL_OPERATIONS_KEY }],
  129. })
  130. );
  131. expect(mock).toBeCalledWith({
  132. url: `${defaultSettings.url}/api/traces?service=jaeger-query&start=1531468681000&end=1531489712000&lookback=custom`,
  133. });
  134. });
  135. it('should convert tags from logfmt format to an object', async () => {
  136. const mock = setupFetchMock({ data: [testResponse] });
  137. const ds = new JaegerDatasource(defaultSettings, timeSrvStub);
  138. await lastValueFrom(
  139. ds.query({
  140. ...defaultQuery,
  141. targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', tags: 'error=true' }],
  142. })
  143. );
  144. expect(mock).toBeCalledWith({
  145. url: `${defaultSettings.url}/api/traces?service=jaeger-query&tags=%7B%22error%22%3A%22true%22%7D&start=1531468681000&end=1531489712000&lookback=custom`,
  146. });
  147. });
  148. it('should resolve templates in traceID', async () => {
  149. const mock = setupFetchMock({ data: [testResponse] });
  150. const ds = new JaegerDatasource(defaultSettings, timeSrvStub);
  151. await lastValueFrom(
  152. ds.query({
  153. ...defaultQuery,
  154. scopedVars: {
  155. $traceid: {
  156. text: 'traceid',
  157. value: '5311b0dd0ca8df3463df93c99cb805a6',
  158. },
  159. },
  160. targets: [
  161. {
  162. query: '$traceid',
  163. refId: '1',
  164. },
  165. ],
  166. })
  167. );
  168. expect(mock).toBeCalledWith({
  169. url: `${defaultSettings.url}/api/traces/5311b0dd0ca8df3463df93c99cb805a6`,
  170. });
  171. });
  172. it('should resolve templates in tags', async () => {
  173. const mock = setupFetchMock({ data: [testResponse] });
  174. const ds = new JaegerDatasource(defaultSettings, timeSrvStub);
  175. await lastValueFrom(
  176. ds.query({
  177. ...defaultQuery,
  178. scopedVars: {
  179. 'error=$error': {
  180. text: 'error',
  181. value: 'error=true',
  182. },
  183. },
  184. targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', tags: 'error=$error' }],
  185. })
  186. );
  187. expect(mock).toBeCalledWith({
  188. url: `${defaultSettings.url}/api/traces?service=jaeger-query&tags=%7B%22error%22%3A%22true%22%7D&start=1531468681000&end=1531489712000&lookback=custom`,
  189. });
  190. });
  191. });
  192. describe('when performing testDataSource', () => {
  193. describe('and call succeeds', () => {
  194. it('should return successfully', async () => {
  195. setupFetchMock({ data: ['service1'] });
  196. const ds = new JaegerDatasource(defaultSettings);
  197. const response = await ds.testDatasource();
  198. expect(response.status).toEqual('success');
  199. expect(response.message).toBe('Data source connected and services found.');
  200. });
  201. });
  202. describe('and call succeeds, but returns no services', () => {
  203. it('should display an error', async () => {
  204. setupFetchMock(undefined);
  205. const ds = new JaegerDatasource(defaultSettings);
  206. const response = await ds.testDatasource();
  207. expect(response.status).toEqual('error');
  208. expect(response.message).toBe(
  209. 'Data source connected, but no services received. Verify that Jaeger is configured properly.'
  210. );
  211. });
  212. });
  213. describe('and call returns error with message', () => {
  214. it('should return the formatted error', async () => {
  215. setupFetchMock(
  216. undefined,
  217. throwError({
  218. statusText: 'Not found',
  219. status: 404,
  220. data: {
  221. message: '404 page not found',
  222. },
  223. })
  224. );
  225. const ds = new JaegerDatasource(defaultSettings);
  226. const response = await ds.testDatasource();
  227. expect(response.status).toEqual('error');
  228. expect(response.message).toBe('Jaeger: Not found. 404. 404 page not found');
  229. });
  230. });
  231. describe('and call returns error without message', () => {
  232. it('should return JSON error', async () => {
  233. setupFetchMock(
  234. undefined,
  235. throwError({
  236. statusText: 'Bad gateway',
  237. status: 502,
  238. data: {
  239. errors: ['Could not connect to Jaeger backend'],
  240. },
  241. })
  242. );
  243. const ds = new JaegerDatasource(defaultSettings);
  244. const response = await ds.testDatasource();
  245. expect(response.status).toEqual('error');
  246. expect(response.message).toBe('Jaeger: Bad gateway. 502. {"errors":["Could not connect to Jaeger backend"]}');
  247. });
  248. });
  249. });
  250. function setupFetchMock(response: any, mock?: any) {
  251. const defaultMock = () => mock ?? of(createFetchResponse(response));
  252. const fetchMock = jest.spyOn(backendSrv, 'fetch');
  253. fetchMock.mockImplementation(defaultMock);
  254. return fetchMock;
  255. }
  256. const defaultSettings: DataSourceInstanceSettings<JaegerJsonData> = {
  257. id: 0,
  258. uid: '0',
  259. type: 'tracing',
  260. name: 'jaeger',
  261. url: 'http://grafana.com',
  262. access: 'proxy',
  263. meta: {
  264. id: 'jaeger',
  265. name: 'jaeger',
  266. type: PluginType.datasource,
  267. info: {} as any,
  268. module: '',
  269. baseUrl: '',
  270. },
  271. jsonData: {
  272. nodeGraph: {
  273. enabled: true,
  274. },
  275. },
  276. };
  277. const defaultQuery: DataQueryRequest<JaegerQuery> = {
  278. requestId: '1',
  279. dashboardId: 0,
  280. interval: '0',
  281. intervalMs: 10,
  282. panelId: 0,
  283. scopedVars: {},
  284. range: {
  285. from: dateTime().subtract(1, 'h'),
  286. to: dateTime(),
  287. raw: { from: '1h', to: 'now' },
  288. },
  289. timezone: 'browser',
  290. app: 'explore',
  291. startTime: 0,
  292. targets: [
  293. {
  294. query: '12345',
  295. refId: '1',
  296. },
  297. ],
  298. };