mirror of https://github.com/grafana/grafana.git
				
				
				
			Loki: Replaces dataSourceRequest with fetch (#27265)
* Loki: Replaces dataSourceRequest with fetch * Update public/app/plugins/datasource/loki/datasource.ts Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com> Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
This commit is contained in:
		
							parent
							
								
									fb2538ce1d
								
							
						
					
					
						commit
						8ec2aa02c6
					
				|  | @ -1,14 +1,19 @@ | ||||||
|  | import { of, Subject } from 'rxjs'; | ||||||
|  | import { first, last, take } from 'rxjs/operators'; | ||||||
|  | import { omit } from 'lodash'; | ||||||
|  | import { AnnotationQueryRequest, DataFrame, DataQueryResponse, dateTime, FieldCache, TimeRange } from '@grafana/data'; | ||||||
|  | import { BackendSrvRequest, FetchResponse } from '@grafana/runtime'; | ||||||
|  | 
 | ||||||
| import LokiDatasource from './datasource'; | import LokiDatasource from './datasource'; | ||||||
| import { LokiQuery, LokiResponse, LokiResultType } from './types'; | import { LokiQuery, LokiResponse, LokiResultType } from './types'; | ||||||
| import { getQueryOptions } from 'test/helpers/getQueryOptions'; | import { getQueryOptions } from 'test/helpers/getQueryOptions'; | ||||||
| import { AnnotationQueryRequest, DataFrame, DataSourceApi, dateTime, FieldCache, TimeRange } from '@grafana/data'; |  | ||||||
| import { TemplateSrv } from 'app/features/templating/template_srv'; | import { TemplateSrv } from 'app/features/templating/template_srv'; | ||||||
| import { makeMockLokiDatasource } from './mocks'; |  | ||||||
| import { of } from 'rxjs'; |  | ||||||
| import omit from 'lodash/omit'; |  | ||||||
| import { backendSrv } from 'app/core/services/backend_srv'; | import { backendSrv } from 'app/core/services/backend_srv'; | ||||||
| import { CustomVariableModel } from '../../../features/variables/types'; | import { CustomVariableModel } from '../../../features/variables/types'; | ||||||
| import { initialCustomVariableModelState } from '../../../features/variables/custom/reducer'; // will use the version in __mocks__
 | import { initialCustomVariableModelState } from '../../../features/variables/custom/reducer'; | ||||||
|  | import { observableTester } from '../../../../test/helpers/observableTester'; | ||||||
|  | import { expect } from '../../../../test/lib/common'; | ||||||
|  | import { makeMockLokiDatasource } from './mocks'; | ||||||
| 
 | 
 | ||||||
| jest.mock('@grafana/runtime', () => ({ | jest.mock('@grafana/runtime', () => ({ | ||||||
|   // @ts-ignore
 |   // @ts-ignore
 | ||||||
|  | @ -27,14 +32,11 @@ jest.mock('app/features/dashboard/services/TimeSrv', () => { | ||||||
|   }; |   }; | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); |  | ||||||
| 
 |  | ||||||
| describe('LokiDatasource', () => { | describe('LokiDatasource', () => { | ||||||
|   const instanceSettings: any = { |   let fetchStream: Subject<FetchResponse>; | ||||||
|     url: 'myloggingurl', |   const fetchMock = jest.spyOn(backendSrv, 'fetch'); | ||||||
|   }; |  | ||||||
| 
 | 
 | ||||||
|   const testResp: { data: LokiResponse } = { |   const testResponse: FetchResponse<LokiResponse> = { | ||||||
|     data: { |     data: { | ||||||
|       data: { |       data: { | ||||||
|         resultType: LokiResultType.Stream, |         resultType: LokiResultType.Stream, | ||||||
|  | @ -47,25 +49,28 @@ describe('LokiDatasource', () => { | ||||||
|       }, |       }, | ||||||
|       status: 'success', |       status: 'success', | ||||||
|     }, |     }, | ||||||
|  |     ok: true, | ||||||
|  |     headers: ({} as unknown) as Headers, | ||||||
|  |     redirected: false, | ||||||
|  |     status: 200, | ||||||
|  |     statusText: 'Success', | ||||||
|  |     type: 'default', | ||||||
|  |     url: '', | ||||||
|  |     config: ({} as unknown) as BackendSrvRequest, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   beforeEach(() => { |   beforeEach(() => { | ||||||
|     jest.clearAllMocks(); |     jest.clearAllMocks(); | ||||||
|     datasourceRequestMock.mockImplementation(() => Promise.resolve()); |     fetchStream = new Subject<FetchResponse>(); | ||||||
|  |     fetchMock.mockImplementation(() => fetchStream.asObservable()); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   const templateSrvMock = ({ |  | ||||||
|     getAdhocFilters: (): any[] => [], |  | ||||||
|     replace: (a: string) => a, |  | ||||||
|   } as unknown) as TemplateSrv; |  | ||||||
| 
 |  | ||||||
|   describe('when creating range query', () => { |   describe('when creating range query', () => { | ||||||
|     let ds: LokiDatasource; |     let ds: LokiDatasource; | ||||||
|     let adjustIntervalSpy: jest.SpyInstance; |     let adjustIntervalSpy: jest.SpyInstance; | ||||||
|  | 
 | ||||||
|     beforeEach(() => { |     beforeEach(() => { | ||||||
|       const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 }; |       ds = createLokiDSForTests(); | ||||||
|       const customSettings = { ...instanceSettings, jsonData: customData }; |  | ||||||
|       ds = new LokiDatasource(customSettings, templateSrvMock); |  | ||||||
|       adjustIntervalSpy = jest.spyOn(ds, 'adjustInterval'); |       adjustIntervalSpy = jest.spyOn(ds, 'adjustInterval'); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  | @ -99,124 +104,161 @@ describe('LokiDatasource', () => { | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   describe('when querying with limits', () => { | ||||||
|  |     const runLimitTest = ({ maxDataPoints, maxLines, expectedLimit, done }: any) => { | ||||||
|  |       let settings: any = { | ||||||
|  |         url: 'myloggingurl', | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       if (Number.isFinite(maxLines!)) { | ||||||
|  |         const customData = { ...(settings.jsonData || {}), maxLines: 20 }; | ||||||
|  |         settings = { ...settings, jsonData: customData }; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const templateSrvMock = ({ | ||||||
|  |         getAdhocFilters: (): any[] => [], | ||||||
|  |         replace: (a: string) => a, | ||||||
|  |       } as unknown) as TemplateSrv; | ||||||
|  | 
 | ||||||
|  |       const ds = new LokiDatasource(settings, templateSrvMock); | ||||||
|  | 
 | ||||||
|  |       const options = getQueryOptions<LokiQuery>({ targets: [{ expr: 'foo', refId: 'B', maxLines: maxDataPoints }] }); | ||||||
|  | 
 | ||||||
|  |       if (Number.isFinite(maxDataPoints!)) { | ||||||
|  |         options.maxDataPoints = maxDataPoints; | ||||||
|  |       } else { | ||||||
|  |         // By default is 500
 | ||||||
|  |         delete options.maxDataPoints; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       observableTester().subscribeAndExpectOnComplete<DataQueryResponse>({ | ||||||
|  |         observable: ds.query(options).pipe(take(1)), | ||||||
|  |         expect: () => { | ||||||
|  |           expect(fetchMock.mock.calls.length).toBe(2); | ||||||
|  |           expect(fetchMock.mock.calls[0][0].url).toContain(`limit=${expectedLimit}`); | ||||||
|  |         }, | ||||||
|  |         done, | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       fetchStream.next(testResponse); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     it('should use default max lines when no limit given', done => { | ||||||
|  |       runLimitTest({ | ||||||
|  |         expectedLimit: 1000, | ||||||
|  |         done, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should use custom max lines if limit is set', done => { | ||||||
|  |       runLimitTest({ | ||||||
|  |         maxLines: 20, | ||||||
|  |         expectedLimit: 20, | ||||||
|  |         done, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should use custom maxDataPoints if set in request', () => { | ||||||
|  |       runLimitTest({ | ||||||
|  |         maxDataPoints: 500, | ||||||
|  |         expectedLimit: 500, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should use datasource maxLimit if maxDataPoints is higher', () => { | ||||||
|  |       runLimitTest({ | ||||||
|  |         maxLines: 20, | ||||||
|  |         maxDataPoints: 500, | ||||||
|  |         expectedLimit: 20, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   describe('when querying', () => { |   describe('when querying', () => { | ||||||
|     let ds: LokiDatasource; |     it('should run range and instant query', done => { | ||||||
|     let testLimit: any; |       const ds = createLokiDSForTests(); | ||||||
| 
 |  | ||||||
|     beforeAll(() => { |  | ||||||
|       testLimit = makeLimitTest(instanceSettings, datasourceRequestMock, templateSrvMock, testResp); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     beforeEach(() => { |  | ||||||
|       const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 }; |  | ||||||
|       const customSettings = { ...instanceSettings, jsonData: customData }; |  | ||||||
|       ds = new LokiDatasource(customSettings, templateSrvMock); |  | ||||||
|       datasourceRequestMock.mockImplementation(() => Promise.resolve(testResp)); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     test('should run range and instant query', async () => { |  | ||||||
|       const options = getQueryOptions<LokiQuery>({ |       const options = getQueryOptions<LokiQuery>({ | ||||||
|         targets: [{ expr: '{job="grafana"}', refId: 'B' }], |         targets: [{ expr: '{job="grafana"}', refId: 'B' }], | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       ds.runInstantQuery = jest.fn(() => of({ data: [] })); |       ds.runInstantQuery = jest.fn(() => of({ data: [] })); | ||||||
|       ds.runRangeQuery = jest.fn(() => of({ data: [] })); |       ds.runRangeQuery = jest.fn(() => of({ data: [] })); | ||||||
|       await ds.query(options).toPromise(); |  | ||||||
| 
 | 
 | ||||||
|  |       observableTester().subscribeAndExpectOnComplete<DataQueryResponse>({ | ||||||
|  |         observable: ds.query(options), | ||||||
|  |         expect: () => { | ||||||
|           expect(ds.runInstantQuery).toBeCalled(); |           expect(ds.runInstantQuery).toBeCalled(); | ||||||
|           expect(ds.runRangeQuery).toBeCalled(); |           expect(ds.runRangeQuery).toBeCalled(); | ||||||
|     }); |         }, | ||||||
| 
 |         done, | ||||||
|     test('should use default max lines when no limit given', () => { |  | ||||||
|       testLimit({ |  | ||||||
|         expectedLimit: 1000, |  | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     test('should use custom max lines if limit is set', () => { |     it('should return series data', done => { | ||||||
|       testLimit({ |       const ds = createLokiDSForTests(); | ||||||
|         maxLines: 20, |  | ||||||
|         expectedLimit: 20, |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     test('should use custom maxDataPoints if set in request', () => { |  | ||||||
|       testLimit({ |  | ||||||
|         maxDataPoints: 500, |  | ||||||
|         expectedLimit: 500, |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     test('should use datasource maxLimit if maxDataPoints is higher', () => { |  | ||||||
|       testLimit({ |  | ||||||
|         maxLines: 20, |  | ||||||
|         maxDataPoints: 500, |  | ||||||
|         expectedLimit: 20, |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     test('should return series data', async () => { |  | ||||||
|       const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 }; |  | ||||||
|       const customSettings = { ...instanceSettings, jsonData: customData }; |  | ||||||
|       const ds = new LokiDatasource(customSettings, templateSrvMock); |  | ||||||
|       datasourceRequestMock.mockImplementation( |  | ||||||
|         jest |  | ||||||
|           .fn() |  | ||||||
|           .mockReturnValueOnce(Promise.resolve(testResp)) |  | ||||||
|           .mockReturnValueOnce(Promise.resolve(omit(testResp, 'data.status'))) |  | ||||||
|       ); |  | ||||||
| 
 |  | ||||||
|       const options = getQueryOptions<LokiQuery>({ |       const options = getQueryOptions<LokiQuery>({ | ||||||
|         targets: [{ expr: '{job="grafana"} |= "foo"', refId: 'B' }], |         targets: [{ expr: '{job="grafana"} |= "foo"', refId: 'B' }], | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       const res = await ds.query(options).toPromise(); |       observableTester().subscribeAndExpectOnNext<DataQueryResponse>({ | ||||||
|  |         observable: ds.query(options).pipe(first()), // first result always comes from runInstantQuery
 | ||||||
|  |         expect: res => { | ||||||
|  |           expect(res).toEqual({ | ||||||
|  |             data: [], | ||||||
|  |             key: 'B_instant', | ||||||
|  |           }); | ||||||
|  |         }, | ||||||
|  |         done, | ||||||
|  |       }); | ||||||
| 
 | 
 | ||||||
|  |       observableTester().subscribeAndExpectOnNext<DataQueryResponse>({ | ||||||
|  |         observable: ds.query(options).pipe(last()), // last result always comes from runRangeQuery
 | ||||||
|  |         expect: res => { | ||||||
|           const dataFrame = res.data[0] as DataFrame; |           const dataFrame = res.data[0] as DataFrame; | ||||||
|           const fieldCache = new FieldCache(dataFrame); |           const fieldCache = new FieldCache(dataFrame); | ||||||
|           expect(fieldCache.getFieldByName('line')?.values.get(0)).toBe('hello'); |           expect(fieldCache.getFieldByName('line')?.values.get(0)).toBe('hello'); | ||||||
|           expect(dataFrame.meta?.limit).toBe(20); |           expect(dataFrame.meta?.limit).toBe(20); | ||||||
|           expect(dataFrame.meta?.searchWords).toEqual(['foo']); |           expect(dataFrame.meta?.searchWords).toEqual(['foo']); | ||||||
|  |         }, | ||||||
|  |         done, | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|     test('should return custom error message when Loki returns escaping error', async () => { |       fetchStream.next(testResponse); | ||||||
|       const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 }; |       fetchStream.next(omit(testResponse, 'data.status')); | ||||||
|       const customSettings = { ...instanceSettings, jsonData: customData }; |     }); | ||||||
|       const ds = new LokiDatasource(customSettings, templateSrvMock); |  | ||||||
| 
 | 
 | ||||||
|       datasourceRequestMock.mockImplementation( |     it('should return custom error message when Loki returns escaping error', done => { | ||||||
|         jest.fn().mockReturnValue( |       const ds = createLokiDSForTests(); | ||||||
|           Promise.reject({ |       const options = getQueryOptions<LokiQuery>({ | ||||||
|  |         targets: [{ expr: '{job="gra\\fana"}', refId: 'B' }], | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       observableTester().subscribeAndExpectOnError<DataQueryResponse>({ | ||||||
|  |         observable: ds.query(options), | ||||||
|  |         expect: err => { | ||||||
|  |           expect(err.data.message).toBe( | ||||||
|  |             'Error: parse error at line 1, col 6: invalid char escape. Make sure that all special characters are escaped with \\. For more information on escaping of special characters visit LogQL documentation at https://github.com/grafana/loki/blob/master/docs/logql.md.' | ||||||
|  |           ); | ||||||
|  |         }, | ||||||
|  |         done, | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       fetchStream.error({ | ||||||
|         data: { |         data: { | ||||||
|           message: 'parse error at line 1, col 6: invalid char escape', |           message: 'parse error at line 1, col 6: invalid char escape', | ||||||
|         }, |         }, | ||||||
|         status: 400, |         status: 400, | ||||||
|         statusText: 'Bad Request', |         statusText: 'Bad Request', | ||||||
|           }) |  | ||||||
|         ) |  | ||||||
|       ); |  | ||||||
|       const options = getQueryOptions<LokiQuery>({ |  | ||||||
|         targets: [{ expr: '{job="gra\\fana"}', refId: 'B' }], |  | ||||||
|       }); |       }); | ||||||
| 
 |  | ||||||
|       try { |  | ||||||
|         await ds.query(options).toPromise(); |  | ||||||
|       } catch (err) { |  | ||||||
|         expect(err.data.message).toBe( |  | ||||||
|           'Error: parse error at line 1, col 6: invalid char escape. Make sure that all special characters are escaped with \\. For more information on escaping of special characters visit LogQL documentation at https://github.com/grafana/loki/blob/master/docs/logql.md.' |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('When interpolating variables', () => { |   describe('when interpolating variables', () => { | ||||||
|     let ds: LokiDatasource; |     let ds: LokiDatasource; | ||||||
|     let variable: CustomVariableModel; |     let variable: CustomVariableModel; | ||||||
| 
 | 
 | ||||||
|     beforeEach(() => { |     beforeEach(() => { | ||||||
|       const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 }; |       ds = createLokiDSForTests(); | ||||||
|       const customSettings = { ...instanceSettings, jsonData: customData }; |  | ||||||
|       ds = new LokiDatasource(customSettings, templateSrvMock); |  | ||||||
|       variable = { ...initialCustomVariableModelState }; |       variable = { ...initialCustomVariableModelState }; | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  | @ -258,86 +300,82 @@ describe('LokiDatasource', () => { | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('when performing testDataSource', () => { |   describe('when performing testDataSource', () => { | ||||||
|     let ds: DataSourceApi<any, any>; |     const getTestContext = () => { | ||||||
|     let result: any; |       const ds = createLokiDSForTests({} as TemplateSrv); | ||||||
|  |       const promise = ds.testDatasource(); | ||||||
|  | 
 | ||||||
|  |       return { promise }; | ||||||
|  |     }; | ||||||
| 
 | 
 | ||||||
|     describe('and call succeeds', () => { |     describe('and call succeeds', () => { | ||||||
|       beforeEach(async () => { |       it('should return successfully', async () => { | ||||||
|         datasourceRequestMock.mockImplementation(async () => { |         const { promise } = getTestContext(); | ||||||
|           return Promise.resolve({ | 
 | ||||||
|  |         fetchStream.next(({ | ||||||
|           status: 200, |           status: 200, | ||||||
|           data: { |           data: { | ||||||
|             values: ['avalue'], |             values: ['avalue'], | ||||||
|           }, |           }, | ||||||
|           }); |         } as unknown) as FetchResponse); | ||||||
|         }); | 
 | ||||||
|         ds = new LokiDatasource(instanceSettings, {} as TemplateSrv); |         fetchStream.complete(); | ||||||
|         result = await ds.testDatasource(); | 
 | ||||||
|       }); |         const result = await promise; | ||||||
| 
 | 
 | ||||||
|       it('should return successfully', () => { |  | ||||||
|         expect(result.status).toBe('success'); |         expect(result.status).toBe('success'); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     describe('and call fails with 401 error', () => { |     describe('and call fails with 401 error', () => { | ||||||
|       let ds: LokiDatasource; |       it('should return error status and a detailed error message', async () => { | ||||||
|       beforeEach(() => { |         const { promise } = getTestContext(); | ||||||
|         datasourceRequestMock.mockImplementation(() => | 
 | ||||||
|           Promise.reject({ |         fetchStream.error({ | ||||||
|           statusText: 'Unauthorized', |           statusText: 'Unauthorized', | ||||||
|           status: 401, |           status: 401, | ||||||
|           data: { |           data: { | ||||||
|             message: 'Unauthorized', |             message: 'Unauthorized', | ||||||
|           }, |           }, | ||||||
|           }) |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 }; |  | ||||||
|         const customSettings = { ...instanceSettings, jsonData: customData }; |  | ||||||
|         ds = new LokiDatasource(customSettings, templateSrvMock); |  | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|       it('should return error status and a detailed error message', async () => { |         const result = await promise; | ||||||
|         const result = await ds.testDatasource(); | 
 | ||||||
|         expect(result.status).toEqual('error'); |         expect(result.status).toEqual('error'); | ||||||
|         expect(result.message).toBe('Loki: Unauthorized. 401. Unauthorized'); |         expect(result.message).toBe('Loki: Unauthorized. 401. Unauthorized'); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     describe('and call fails with 404 error', () => { |     describe('and call fails with 404 error', () => { | ||||||
|       beforeEach(async () => { |       it('should return error status and a detailed error message', async () => { | ||||||
|         datasourceRequestMock.mockImplementation(() => |         const { promise } = getTestContext(); | ||||||
|           Promise.reject({ | 
 | ||||||
|  |         fetchStream.error({ | ||||||
|           statusText: 'Not found', |           statusText: 'Not found', | ||||||
|           status: 404, |           status: 404, | ||||||
|             data: '404 page not found', |           data: { | ||||||
|           }) |             message: '404 page not found', | ||||||
|         ); |           }, | ||||||
|         ds = new LokiDatasource(instanceSettings, {} as TemplateSrv); |  | ||||||
|         result = await ds.testDatasource(); |  | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|       it('should return error status and a detailed error message', () => { |         const result = await promise; | ||||||
|  | 
 | ||||||
|         expect(result.status).toEqual('error'); |         expect(result.status).toEqual('error'); | ||||||
|         expect(result.message).toBe('Loki: Not found. 404. 404 page not found'); |         expect(result.message).toBe('Loki: Not found. 404. 404 page not found'); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     describe('and call fails with 502 error', () => { |     describe('and call fails with 502 error', () => { | ||||||
|       beforeEach(async () => { |       it('should return error status and a detailed error message', async () => { | ||||||
|         datasourceRequestMock.mockImplementation(() => |         const { promise } = getTestContext(); | ||||||
|           Promise.reject({ | 
 | ||||||
|  |         fetchStream.error({ | ||||||
|           statusText: 'Bad Gateway', |           statusText: 'Bad Gateway', | ||||||
|           status: 502, |           status: 502, | ||||||
|           data: '', |           data: '', | ||||||
|           }) |  | ||||||
|         ); |  | ||||||
|         ds = new LokiDatasource(instanceSettings, {} as TemplateSrv); |  | ||||||
|         result = await ds.testDatasource(); |  | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|       it('should return error status and a detailed error message', () => { |         const result = await promise; | ||||||
|  | 
 | ||||||
|         expect(result.status).toEqual('error'); |         expect(result.status).toEqual('error'); | ||||||
|         expect(result.message).toBe('Loki: Bad Gateway. 502'); |         expect(result.message).toBe('Loki: Bad Gateway. 502'); | ||||||
|       }); |       }); | ||||||
|  | @ -345,28 +383,35 @@ describe('LokiDatasource', () => { | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('when creating a range query', () => { |   describe('when creating a range query', () => { | ||||||
|     const ds = new LokiDatasource(instanceSettings, templateSrvMock); |  | ||||||
|     const query: LokiQuery = { expr: 'foo', refId: 'bar' }; |  | ||||||
| 
 |  | ||||||
|     // Loki v1 API has an issue with float step parameters, can be removed when API is fixed
 |     // Loki v1 API has an issue with float step parameters, can be removed when API is fixed
 | ||||||
|     it('should produce an integer step parameter', () => { |     it('should produce an integer step parameter', () => { | ||||||
|  |       const ds = createLokiDSForTests(); | ||||||
|  |       const query: LokiQuery = { expr: 'foo', refId: 'bar' }; | ||||||
|       const range: TimeRange = { |       const range: TimeRange = { | ||||||
|         from: dateTime(0), |         from: dateTime(0), | ||||||
|         to: dateTime(1e9 + 1), |         to: dateTime(1e9 + 1), | ||||||
|         raw: { from: '0', to: '1000000001' }, |         raw: { from: '0', to: '1000000001' }, | ||||||
|       }; |       }; | ||||||
|  | 
 | ||||||
|       // Odd timerange/interval combination that would lead to a float step
 |       // Odd timerange/interval combination that would lead to a float step
 | ||||||
|       const options = { range, intervalMs: 2000 }; |       const options = { range, intervalMs: 2000 }; | ||||||
|  | 
 | ||||||
|       expect(Number.isInteger(ds.createRangeQuery(query, options as any).step!)).toBeTruthy(); |       expect(Number.isInteger(ds.createRangeQuery(query, options as any).step!)).toBeTruthy(); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('annotationQuery', () => { |   describe('when calling annotationQuery', () => { | ||||||
|  |     const getTestContext = () => { | ||||||
|  |       const query = makeAnnotationQueryRequest(); | ||||||
|  |       const ds = createLokiDSForTests(); | ||||||
|  |       const promise = ds.annotationQuery(query); | ||||||
|  | 
 | ||||||
|  |       return { promise }; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|     it('should transform the loki data to annotation response', async () => { |     it('should transform the loki data to annotation response', async () => { | ||||||
|       const ds = new LokiDatasource(instanceSettings, templateSrvMock); |       const { promise } = getTestContext(); | ||||||
|       datasourceRequestMock.mockImplementation( |       const response: FetchResponse = ({ | ||||||
|         jest.fn().mockReturnValueOnce( |  | ||||||
|           Promise.resolve({ |  | ||||||
|         data: { |         data: { | ||||||
|           data: { |           data: { | ||||||
|             resultType: LokiResultType.Stream, |             resultType: LokiResultType.Stream, | ||||||
|  | @ -388,13 +433,13 @@ describe('LokiDatasource', () => { | ||||||
|           }, |           }, | ||||||
|           status: 'success', |           status: 'success', | ||||||
|         }, |         }, | ||||||
|           }) |       } as unknown) as FetchResponse; | ||||||
|         ) |  | ||||||
|       ); |  | ||||||
| 
 | 
 | ||||||
|       const query = makeAnnotationQueryRequest(); |       fetchStream.next(response); | ||||||
|  |       fetchStream.complete(); | ||||||
|  | 
 | ||||||
|  |       const res = await promise; | ||||||
| 
 | 
 | ||||||
|       const res = await ds.annotationQuery(query); |  | ||||||
|       expect(res.length).toBe(2); |       expect(res.length).toBe(2); | ||||||
|       expect(res[0].text).toBe('hello'); |       expect(res[0].text).toBe('hello'); | ||||||
|       expect(res[0].tags).toEqual(['value']); |       expect(res[0].tags).toEqual(['value']); | ||||||
|  | @ -405,98 +450,72 @@ describe('LokiDatasource', () => { | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('metricFindQuery', () => { |   describe('metricFindQuery', () => { | ||||||
|     const ds = new LokiDatasource(instanceSettings, templateSrvMock); |     const getTestContext = (mock: LokiDatasource) => { | ||||||
|  |       const ds = createLokiDSForTests(); | ||||||
|  |       ds.getVersion = mock.getVersion; | ||||||
|  |       ds.metadataRequest = mock.metadataRequest; | ||||||
|  | 
 | ||||||
|  |       return { ds }; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|     const mocks = makeMetadataAndVersionsMocks(); |     const mocks = makeMetadataAndVersionsMocks(); | ||||||
| 
 | 
 | ||||||
|     mocks.forEach((mock, index) => { |     mocks.forEach((mock, index) => { | ||||||
|       it(`should return label names for Loki v${index}`, async () => { |       it(`should return label names for Loki v${index}`, async () => { | ||||||
|         ds.getVersion = mock.getVersion; |         const { ds } = getTestContext(mock); | ||||||
|         ds.metadataRequest = mock.metadataRequest; |  | ||||||
|         const query = 'label_names()'; |  | ||||||
|         const res = await ds.metricFindQuery(query); |  | ||||||
|         expect(res[0].text).toEqual('label1'); |  | ||||||
|         expect(res[1].text).toEqual('label2'); |  | ||||||
|         expect(res.length).toBe(2); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 | 
 | ||||||
|     mocks.forEach((mock, index) => { |         const res = await ds.metricFindQuery('label_names()'); | ||||||
|       it(`should return label names for Loki v${index}`, async () => { | 
 | ||||||
|         ds.getVersion = mock.getVersion; |         expect(res).toEqual([{ text: 'label1' }, { text: 'label2' }]); | ||||||
|         ds.metadataRequest = mock.metadataRequest; |  | ||||||
|         const query = 'label_names()'; |  | ||||||
|         const res = await ds.metricFindQuery(query); |  | ||||||
|         expect(res[0].text).toEqual('label1'); |  | ||||||
|         expect(res[1].text).toEqual('label2'); |  | ||||||
|         expect(res.length).toBe(2); |  | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     mocks.forEach((mock, index) => { |     mocks.forEach((mock, index) => { | ||||||
|       it(`should return label values for Loki v${index}`, async () => { |       it(`should return label values for Loki v${index}`, async () => { | ||||||
|         ds.getVersion = mock.getVersion; |         const { ds } = getTestContext(mock); | ||||||
|         ds.metadataRequest = mock.metadataRequest; | 
 | ||||||
|         const query = 'label_values(label1)'; |         const res = await ds.metricFindQuery('label_values(label1)'); | ||||||
|         const res = await ds.metricFindQuery(query); | 
 | ||||||
|         expect(res[0].text).toEqual('value1'); |         expect(res).toEqual([{ text: 'value1' }, { text: 'value2' }]); | ||||||
|         expect(res[1].text).toEqual('value2'); |  | ||||||
|         expect(res.length).toBe(2); |  | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     mocks.forEach((mock, index) => { |     mocks.forEach((mock, index) => { | ||||||
|       it(`should return empty array when incorrect query for Loki v${index}`, async () => { |       it(`should return empty array when incorrect query for Loki v${index}`, async () => { | ||||||
|         ds.getVersion = mock.getVersion; |         const { ds } = getTestContext(mock); | ||||||
|         ds.metadataRequest = mock.metadataRequest; | 
 | ||||||
|         const query = 'incorrect_query'; |         const res = await ds.metricFindQuery('incorrect_query'); | ||||||
|         const res = await ds.metricFindQuery(query); | 
 | ||||||
|         expect(res.length).toBe(0); |         expect(res).toEqual([]); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     mocks.forEach((mock, index) => { |     mocks.forEach((mock, index) => { | ||||||
|       it(`should return label names according to provided rangefor Loki v${index}`, async () => { |       it(`should return label names according to provided rangefor Loki v${index}`, async () => { | ||||||
|         ds.getVersion = mock.getVersion; |         const { ds } = getTestContext(mock); | ||||||
|         ds.metadataRequest = mock.metadataRequest; | 
 | ||||||
|         const query = 'label_names()'; |         const res = await ds.metricFindQuery('label_names()', { range: { from: new Date(2), to: new Date(3) } }); | ||||||
|         const res = await ds.metricFindQuery(query, { | 
 | ||||||
|           range: { from: new Date(2), to: new Date(3) }, |         expect(res).toEqual([{ text: 'label1' }]); | ||||||
|         }); |  | ||||||
|         expect(res[0].text).toEqual('label1'); |  | ||||||
|         expect(res.length).toBe(1); |  | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| type LimitTestArgs = { | function createLokiDSForTests( | ||||||
|   maxDataPoints?: number; |   templateSrvMock = ({ | ||||||
|   maxLines?: number; |     getAdhocFilters: (): any[] => [], | ||||||
|   expectedLimit: number; |     replace: (a: string) => a, | ||||||
|  |   } as unknown) as TemplateSrv | ||||||
|  | ): LokiDatasource { | ||||||
|  |   const instanceSettings: any = { | ||||||
|  |     url: 'myloggingurl', | ||||||
|   }; |   }; | ||||||
| function makeLimitTest(instanceSettings: any, datasourceRequestMock: any, templateSrvMock: any, testResp: any) { | 
 | ||||||
|   return ({ maxDataPoints, maxLines, expectedLimit }: LimitTestArgs) => { |  | ||||||
|     let settings = instanceSettings; |  | ||||||
|     if (Number.isFinite(maxLines!)) { |  | ||||||
|   const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 }; |   const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 }; | ||||||
|       settings = { ...instanceSettings, jsonData: customData }; |   const customSettings = { ...instanceSettings, jsonData: customData }; | ||||||
|     } |  | ||||||
|     const ds = new LokiDatasource(settings, templateSrvMock); |  | ||||||
|     datasourceRequestMock.mockImplementation(() => Promise.resolve(testResp)); |  | ||||||
| 
 | 
 | ||||||
|     const options = getQueryOptions<LokiQuery>({ targets: [{ expr: 'foo', refId: 'B', maxLines: maxDataPoints }] }); |   return new LokiDatasource(customSettings, templateSrvMock); | ||||||
|     if (Number.isFinite(maxDataPoints!)) { |  | ||||||
|       options.maxDataPoints = maxDataPoints; |  | ||||||
|     } else { |  | ||||||
|       // By default is 500
 |  | ||||||
|       delete options.maxDataPoints; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     ds.query(options); |  | ||||||
| 
 |  | ||||||
|     expect(datasourceRequestMock.mock.calls.length).toBe(2); |  | ||||||
|     expect(datasourceRequestMock.mock.calls[0][0].url).toContain(`limit=${expectedLimit}`); |  | ||||||
|   }; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function makeAnnotationQueryRequest(): AnnotationQueryRequest<LokiQuery> { | function makeAnnotationQueryRequest(): AnnotationQueryRequest<LokiQuery> { | ||||||
|  |  | ||||||
|  | @ -1,41 +1,43 @@ | ||||||
| // Libraries
 | // Libraries
 | ||||||
| import { isEmpty, map as lodashMap, cloneDeep } from 'lodash'; | import { cloneDeep, isEmpty, map as lodashMap } from 'lodash'; | ||||||
| import { Observable, from, merge, of } from 'rxjs'; | import { merge, Observable, of } from 'rxjs'; | ||||||
| import { map, catchError, switchMap } from 'rxjs/operators'; | import { catchError, map, switchMap } from 'rxjs/operators'; | ||||||
| 
 |  | ||||||
| // Services & Utils
 |  | ||||||
| import { DataFrame, dateMath, FieldCache, QueryResultMeta, TimeRange } from '@grafana/data'; |  | ||||||
| import { getBackendSrv, BackendSrvRequest, FetchError } from '@grafana/runtime'; |  | ||||||
| import { addLabelToQuery } from 'app/plugins/datasource/prometheus/add_label_to_query'; |  | ||||||
| import { TemplateSrv } from 'app/features/templating/template_srv'; |  | ||||||
| import { convertToWebSocketUrl } from 'app/core/utils/explore'; |  | ||||||
| import { lokiResultsToTableModel, processRangeQueryResponse, lokiStreamResultToDataFrame } from './result_transformer'; |  | ||||||
| import { getHighlighterExpressionsFromQuery } from './query_utils'; |  | ||||||
| import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; |  | ||||||
| 
 | 
 | ||||||
| // Types
 | // Types
 | ||||||
| import { | import { | ||||||
|   LogRowModel, |  | ||||||
|   DateTime, |  | ||||||
|   LoadingState, |  | ||||||
|   AnnotationEvent, |   AnnotationEvent, | ||||||
|  |   AnnotationQueryRequest, | ||||||
|  |   DataFrame, | ||||||
|   DataFrameView, |   DataFrameView, | ||||||
|   PluginMeta, |  | ||||||
|   DataSourceApi, |  | ||||||
|   DataSourceInstanceSettings, |  | ||||||
|   DataQueryError, |   DataQueryError, | ||||||
|   DataQueryRequest, |   DataQueryRequest, | ||||||
|   DataQueryResponse, |   DataQueryResponse, | ||||||
|   AnnotationQueryRequest, |   DataSourceApi, | ||||||
|  |   DataSourceInstanceSettings, | ||||||
|  |   dateMath, | ||||||
|  |   DateTime, | ||||||
|  |   FieldCache, | ||||||
|  |   LoadingState, | ||||||
|  |   LogRowModel, | ||||||
|  |   PluginMeta, | ||||||
|  |   QueryResultMeta, | ||||||
|   ScopedVars, |   ScopedVars, | ||||||
|  |   TimeRange, | ||||||
| } from '@grafana/data'; | } from '@grafana/data'; | ||||||
|  | import { BackendSrvRequest, FetchError, getBackendSrv } from '@grafana/runtime'; | ||||||
|  | import { addLabelToQuery } from 'app/plugins/datasource/prometheus/add_label_to_query'; | ||||||
|  | import { TemplateSrv } from 'app/features/templating/template_srv'; | ||||||
|  | import { convertToWebSocketUrl } from 'app/core/utils/explore'; | ||||||
|  | import { lokiResultsToTableModel, lokiStreamResultToDataFrame, processRangeQueryResponse } from './result_transformer'; | ||||||
|  | import { getHighlighterExpressionsFromQuery } from './query_utils'; | ||||||
|  | import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; | ||||||
| 
 | 
 | ||||||
| import { | import { | ||||||
|   LokiQuery, |  | ||||||
|   LokiOptions, |   LokiOptions, | ||||||
|  |   LokiQuery, | ||||||
|  |   LokiRangeQueryRequest, | ||||||
|   LokiResponse, |   LokiResponse, | ||||||
|   LokiResultType, |   LokiResultType, | ||||||
|   LokiRangeQueryRequest, |  | ||||||
|   LokiStreamResponse, |   LokiStreamResponse, | ||||||
| } from './types'; | } from './types'; | ||||||
| import { LiveStreams, LokiLiveTarget } from './live_streams'; | import { LiveStreams, LokiLiveTarget } from './live_streams'; | ||||||
|  | @ -79,7 +81,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> { | ||||||
|       url, |       url, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return from(getBackendSrv().datasourceRequest(req)); |     return getBackendSrv().fetch<Record<string, any>>(req); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   query(options: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> { |   query(options: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,68 @@ | ||||||
|  | import { Observable } from 'rxjs'; | ||||||
|  | 
 | ||||||
|  | interface ObservableTester<T> { | ||||||
|  |   observable: Observable<T>; | ||||||
|  |   done: jest.DoneCallback; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface SubscribeAndExpectOnNext<T> extends ObservableTester<T> { | ||||||
|  |   expect: (value: T) => void; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface SubscribeAndExpectOnComplete<T> extends ObservableTester<T> { | ||||||
|  |   expect: () => void; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface SubscribeAndExpectOnError<T> extends ObservableTester<T> { | ||||||
|  |   expect: (err: any) => void; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const observableTester = () => { | ||||||
|  |   const subscribeAndExpectOnNext = <T>({ observable, expect, done }: SubscribeAndExpectOnNext<T>): void => { | ||||||
|  |     observable.subscribe({ | ||||||
|  |       next: value => { | ||||||
|  |         try { | ||||||
|  |           expect(value); | ||||||
|  |         } catch (err) { | ||||||
|  |           done.fail(err); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       error: err => done.fail(err), | ||||||
|  |       complete: () => done(), | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const subscribeAndExpectOnComplete = <T>({ observable, expect, done }: SubscribeAndExpectOnComplete<T>): void => { | ||||||
|  |     observable.subscribe({ | ||||||
|  |       next: () => {}, | ||||||
|  |       error: err => done.fail(err), | ||||||
|  |       complete: () => { | ||||||
|  |         try { | ||||||
|  |           expect(); | ||||||
|  |           done(); | ||||||
|  |         } catch (err) { | ||||||
|  |           done.fail(err); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const subscribeAndExpectOnError = <T>({ observable, expect, done }: SubscribeAndExpectOnError<T>): void => { | ||||||
|  |     observable.subscribe({ | ||||||
|  |       next: () => {}, | ||||||
|  |       error: err => { | ||||||
|  |         try { | ||||||
|  |           expect(err); | ||||||
|  |           done(); | ||||||
|  |         } catch (err) { | ||||||
|  |           done.fail(err); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       complete: () => { | ||||||
|  |         done(); | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   return { subscribeAndExpectOnNext, subscribeAndExpectOnComplete, subscribeAndExpectOnError }; | ||||||
|  | }; | ||||||
		Loading…
	
		Reference in New Issue