Skip to content

Commit

Permalink
Add data source health check (#71)
Browse files Browse the repository at this point in the history
* Add data source health check

* Update CHANGELOG.md

* Fix pr comments

---------

Co-authored-by: Mikhail Volkov <[email protected]>
  • Loading branch information
asimonok and mikhail-vl authored Jul 16, 2024
1 parent 0fd6d73 commit 50bf315
Show file tree
Hide file tree
Showing 15 changed files with 246 additions and 69 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Updated minimum Grafana version to 10.1.0 (#72)
- Added get dashboards meta request (#70)
- Added data source health check (#71)

## 3.0.0 (2024-07-11)

Expand Down
25 changes: 25 additions & 0 deletions src/api/datasources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,31 @@ describe('Data Sources Api', () => {
expect(result[0].fields[0].values).toEqual([1, 2]);
});

it('Should check health', async () => {
fetchRequestMock = jest
.fn()
.mockImplementationOnce(() => getResponse(response))
/**
* First health
*/
.mockImplementationOnce(() =>
getResponse({
status: 200,
})
)
/**
* Second health
*/
.mockImplementationOnce(() => getErrorResponse());
const result = await api.features.datasources.getFrame({
...query,
datasourceHealth: true,
});
expect(result?.length).toEqual(1);
expect(result[0].fields.length).toEqual(12);
expect(result[0].fields[11].values).toEqual([200, 500]);
});

it('Should handle getDataSourcesFrame request with no data', async () => {
fetchRequestMock = jest.fn().mockImplementation(() =>
getResponse({
Expand Down
155 changes: 95 additions & 60 deletions src/api/datasources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getBackendSrv } from '@grafana/runtime';
import { lastValueFrom } from 'rxjs';

import { MESSAGES } from '../constants';
import { Query, RequestType } from '../types';
import { DataSourceHealthMessage, FieldMapper, Query, RequestType } from '../types';
import { convertToFrame, notifyError } from '../utils';
import { BaseApi } from './base';

Expand Down Expand Up @@ -36,79 +36,114 @@ export class DataSources extends BaseApi {
return response.data;
};

/**
* Get Health Status
*/
private getHealthStatus = async (id: string): Promise<number> => {
/**
* Fetch
*/
const response = await lastValueFrom(
getBackendSrv().fetch<DataSourceHealthMessage>({
method: 'GET',
url: `${this.api.instanceSettings.url}/api/datasources/uid/${id}/health`,
})
).catch((reason) => reason);

return response?.status || 500;
};

/**
* Get Data Sources Frame
*/
getFrame = async (query: Query): Promise<DataFrame[]> => {
const datasources = await this.getAll();

if (!datasources.length) {
return [];
}

const datasourcesHealth = query.datasourceHealth
? await Promise.all(datasources.map((datasource) => this.getHealthStatus(datasource.uid)))
: [];

const fields: Array<FieldMapper<{ ds: DataSourceSettings; health: number }>> = [
{
name: 'Id',
type: FieldType.number,
getValue: (item) => item.ds.id,
},
{
name: 'Org Id',
type: FieldType.number,
getValue: (item) => item.ds.orgId,
},
{
name: 'UID',
type: FieldType.string,
getValue: (item) => item.ds.uid,
},
{
name: 'Name',
type: FieldType.string,
getValue: (item) => item.ds.name,
},
{
name: 'Type',
type: FieldType.string,
getValue: (item) => item.ds.type,
},
{
name: 'Type Logo URL',
type: FieldType.string,
getValue: (item) => item.ds.typeLogoUrl,
},
{
name: 'Type Name',
type: FieldType.string,
getValue: (item) => item.ds.typeName,
},
{
name: 'Is Default',
type: FieldType.boolean,
getValue: (item) => item.ds.isDefault,
},
{
name: 'Read Only',
type: FieldType.boolean,
getValue: (item) => item.ds.readOnly,
},
{
name: 'URL',
type: FieldType.string,
getValue: (item) => item.ds.url,
},
{
name: 'User',
type: FieldType.string,
getValue: (item) => item.ds.user,
},
];

if (query.datasourceHealth) {
fields.push({
name: 'Health Status',
type: FieldType.number,
getValue: (item) => item.health,
});
}

/**
* Create Frame
*/
const frame = convertToFrame<DataSourceSettings>({
const frame = convertToFrame<{ ds: DataSourceSettings; health: number }>({
name: RequestType.DATASOURCES,
refId: query.refId,
fields: [
{
name: 'Id',
type: FieldType.number,
getValue: (item) => item.id,
},
{
name: 'Org Id',
type: FieldType.number,
getValue: (item) => item.orgId,
},
{
name: 'UID',
type: FieldType.string,
getValue: (item) => item.uid,
},
{
name: 'Name',
type: FieldType.string,
getValue: (item) => item.name,
},
{
name: 'Type',
type: FieldType.string,
getValue: (item) => item.type,
},
{
name: 'Type Logo URL',
type: FieldType.string,
getValue: (item) => item.typeLogoUrl,
},
{
name: 'Type Name',
type: FieldType.string,
getValue: (item) => item.typeName,
},
{
name: 'Is Default',
type: FieldType.boolean,
getValue: (item) => item.isDefault,
},
{
name: 'Read Only',
type: FieldType.boolean,
getValue: (item) => item.readOnly,
},
{
name: 'URL',
type: FieldType.string,
getValue: (item) => item.url,
},
{
name: 'User',
type: FieldType.string,
getValue: (item) => item.user,
},
],
items: datasources,
fields,
items: datasources.map((ds, index) => ({
ds,
health: datasourcesHealth[index],
})),
});

return [frame];
Expand Down
36 changes: 36 additions & 0 deletions src/components/QueryEditor/QueryEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -340,4 +340,40 @@ describe('QueryEditor', () => {
});
});
});

/**
* Data Sources
*/
describe('Data Sources', () => {
const onChange = jest.fn();

it('Should allow to update check health', () => {
render(
<QueryEditor
datasource={datasource as any}
query={
{
requestType: RequestType.DATASOURCES,
datasourceHealth: false,
} as any
}
onRunQuery={onRunQuery}
onChange={onChange}
/>
);

expect(selectors.fieldDatasourcesCheckHealth()).toBeInTheDocument();

const enabledOption = getSelectors(within(selectors.fieldDatasourcesCheckHealth())).option(false, 'true');
expect(enabledOption).toBeInTheDocument();

fireEvent.click(enabledOption);

expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
datasourceHealth: true,
})
);
});
});
});
26 changes: 26 additions & 0 deletions src/components/QueryEditor/QueryEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ANNOTATION_RULES_OPTIONS,
ANNOTATION_STATES_OPTIONS,
ANNOTATION_TYPE_OPTIONS,
BOOLEAN_OPTIONS,
DEFAULT_QUERY,
REQUEST_TYPE_OPTIONS,
TEST_IDS,
Expand All @@ -23,6 +24,7 @@ import {
Query,
RequestType,
} from '../../types';
import { getOptionsWithTestId } from '../../utils';

/**
* Editor Properties
Expand Down Expand Up @@ -163,6 +165,17 @@ export const QueryEditor: React.FC<Props> = ({ onChange, onRunQuery, query: rawQ
);
}, [datasource.api.availableRequestTypes]);

/**
* Change Query Field
*/
const onChangeQueryField = useCallback(
(name: keyof Query, value: Query[typeof name]) => {
onChange({ ...rawQuery, [name]: value });
onRunQuery();
},
[onChange, onRunQuery, rawQuery]
);

/**
* Render
*/
Expand Down Expand Up @@ -278,6 +291,19 @@ export const QueryEditor: React.FC<Props> = ({ onChange, onRunQuery, query: rawQ
)}
</>
)}
{query.requestType === RequestType.DATASOURCES && (
<InlineFieldRow>
<InlineField label="Check Health" data-testid={TEST_IDS.queryEditor.fieldDatasourcesCheckHealth}>
<RadioButtonGroup
options={getOptionsWithTestId(BOOLEAN_OPTIONS, TEST_IDS.queryEditor.option)}
value={query.datasourceHealth}
onChange={(value) => {
onChangeQueryField('datasourceHealth', value);
}}
/>
</InlineField>
</InlineFieldRow>
)}
</>
);
};
1 change: 1 addition & 0 deletions src/constants/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export const DEFAULT_QUERY: Partial<Query> = {
annotationRange: AnnotationRange.NONE,
annotationRules: true,
annotationType: AnnotationType.ALL,
datasourceHealth: false,
requestType: RequestType.NONE,
};
14 changes: 14 additions & 0 deletions src/constants/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,17 @@ export const REQUEST_TYPE_OPTIONS: Array<SelectableValue<RequestType>> = [
value: RequestType.NONE,
},
];

/**
* Boolean Options
*/
export const BOOLEAN_OPTIONS: Array<SelectableValue<boolean>> = [
{
value: true,
label: 'Enabled',
},
{
value: false,
label: 'Disabled',
},
];
2 changes: 2 additions & 0 deletions src/constants/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const TEST_IDS = {
fieldRequestModelOption: (name: string) => `config-editor field-request-mode-option-${name}`,
},
queryEditor: {
option: (name: unknown) => `query-editor option-${name}`,
fieldAnnotationDashboardContainer: 'data-testid query-editor field-annotation-dashboard-container',
fieldAnnotationDashboardOption: (name: string) => `query-editor field-annotation-dashboard-option-${name}`,
fieldAnnotationRulesContainer: 'data-testid query-editor field-annotation-rules-container',
Expand All @@ -23,5 +24,6 @@ export const TEST_IDS = {
fieldAnnotationTypeContainer: 'data-testid query-editor field-annotation-type-container',
fieldAnnotationTypeOption: (name: string) => `query-editor field-annotation-type-option-${name}`,
fieldRequest: 'query-editor field-request',
fieldDatasourcesCheckHealth: 'data-testid query-editor field-datasources-check-health',
},
};
14 changes: 14 additions & 0 deletions src/types/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,17 @@ export interface SecureJsonData {
*/
token?: string;
}

/**
* Data Source Health Message
*/
export type DataSourceHealthMessage =
| {
message: string;
status: string;
}
| {
message: string;
error: string;
status: string;
};
10 changes: 10 additions & 0 deletions src/types/frame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { FieldType } from '@grafana/data';

/**
* Field Mapper
*/
export type FieldMapper<TItem> = {
name: string;
type: FieldType;
getValue: (item: TItem) => unknown;
};
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './api';
export * from './auth-key';
export * from './dashboard';
export * from './datasource';
export * from './frame';
export * from './health';
export * from './query';
export * from './user';
Loading

0 comments on commit 50bf315

Please sign in to comment.